Unverified Commit 27ef37a3 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

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 ```
parents 70b01d75 a9765bce
...@@ -506,11 +506,9 @@ func BuildAdmissionPluginInitializer(s *options.ServerRunOptions, client interna ...@@ -506,11 +506,9 @@ func BuildAdmissionPluginInitializer(s *options.ServerRunOptions, client interna
// TODO: use a dynamic restmapper. See https://github.com/kubernetes/kubernetes/pull/42615. // TODO: use a dynamic restmapper. See https://github.com/kubernetes/kubernetes/pull/42615.
restMapper := legacyscheme.Registry.RESTMapper() restMapper := legacyscheme.Registry.RESTMapper()
// NOTE: we do not provide informers to the quota registry because admission level decisions quotaConfiguration := quotainstall.NewQuotaConfigurationForAdmission()
// do not require us to open watches for all items tracked by quota.
quotaRegistry := quotainstall.NewRegistry(nil, nil)
pluginInitializer := kubeapiserveradmission.NewPluginInitializer(client, sharedInformers, cloudConfig, restMapper, quotaRegistry, webhookAuthWrapper, serviceResolver) pluginInitializer := kubeapiserveradmission.NewPluginInitializer(client, sharedInformers, cloudConfig, restMapper, quotaConfiguration, webhookAuthWrapper, serviceResolver)
return pluginInitializer, nil return pluginInitializer, nil
} }
......
...@@ -24,7 +24,6 @@ go_library( ...@@ -24,7 +24,6 @@ go_library(
importpath = "k8s.io/kubernetes/cmd/kube-controller-manager/app", importpath = "k8s.io/kubernetes/cmd/kube-controller-manager/app",
deps = [ deps = [
"//cmd/kube-controller-manager/app/options:go_default_library", "//cmd/kube-controller-manager/app/options:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/install:go_default_library", "//pkg/api/install:go_default_library",
"//pkg/api/legacyscheme:go_default_library", "//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/apps/install:go_default_library", "//pkg/apis/apps/install:go_default_library",
...@@ -78,6 +77,7 @@ go_library( ...@@ -78,6 +77,7 @@ go_library(
"//pkg/controller/volume/expand:go_default_library", "//pkg/controller/volume/expand:go_default_library",
"//pkg/controller/volume/persistentvolume:go_default_library", "//pkg/controller/volume/persistentvolume:go_default_library",
"//pkg/features:go_default_library", "//pkg/features:go_default_library",
"//pkg/quota/generic:go_default_library",
"//pkg/quota/install:go_default_library", "//pkg/quota/install:go_default_library",
"//pkg/serviceaccount:go_default_library", "//pkg/serviceaccount:go_default_library",
"//pkg/util/configz:go_default_library", "//pkg/util/configz:go_default_library",
......
...@@ -36,7 +36,6 @@ import ( ...@@ -36,7 +36,6 @@ import (
cacheddiscovery "k8s.io/client-go/discovery/cached" cacheddiscovery "k8s.io/client-go/discovery/cached"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/controller"
endpointcontroller "k8s.io/kubernetes/pkg/controller/endpoint" endpointcontroller "k8s.io/kubernetes/pkg/controller/endpoint"
...@@ -55,6 +54,7 @@ import ( ...@@ -55,6 +54,7 @@ import (
"k8s.io/kubernetes/pkg/controller/volume/expand" "k8s.io/kubernetes/pkg/controller/volume/expand"
persistentvolumecontroller "k8s.io/kubernetes/pkg/controller/volume/persistentvolume" persistentvolumecontroller "k8s.io/kubernetes/pkg/controller/volume/persistentvolume"
"k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/quota/generic"
quotainstall "k8s.io/kubernetes/pkg/quota/install" quotainstall "k8s.io/kubernetes/pkg/quota/install"
"k8s.io/kubernetes/pkg/util/metrics" "k8s.io/kubernetes/pkg/util/metrics"
) )
...@@ -240,31 +240,34 @@ func startPodGCController(ctx ControllerContext) (bool, error) { ...@@ -240,31 +240,34 @@ func startPodGCController(ctx ControllerContext) (bool, error) {
func startResourceQuotaController(ctx ControllerContext) (bool, error) { func startResourceQuotaController(ctx ControllerContext) (bool, error) {
resourceQuotaControllerClient := ctx.ClientBuilder.ClientOrDie("resourcequota-controller") resourceQuotaControllerClient := ctx.ClientBuilder.ClientOrDie("resourcequota-controller")
resourceQuotaRegistry := quotainstall.NewRegistry(resourceQuotaControllerClient, ctx.InformerFactory) discoveryFunc := resourceQuotaControllerClient.Discovery().ServerPreferredNamespacedResources
groupKindsToReplenish := []schema.GroupKind{ listerFuncForResource := generic.ListerFuncForResourceFunc(ctx.InformerFactory.ForResource)
api.Kind("Pod"), quotaConfiguration := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource)
api.Kind("Service"),
api.Kind("ReplicationController"),
api.Kind("PersistentVolumeClaim"),
api.Kind("Secret"),
api.Kind("ConfigMap"),
}
resourceQuotaControllerOptions := &resourcequotacontroller.ResourceQuotaControllerOptions{ resourceQuotaControllerOptions := &resourcequotacontroller.ResourceQuotaControllerOptions{
QuotaClient: resourceQuotaControllerClient.CoreV1(), QuotaClient: resourceQuotaControllerClient.CoreV1(),
ResourceQuotaInformer: ctx.InformerFactory.Core().V1().ResourceQuotas(), ResourceQuotaInformer: ctx.InformerFactory.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.StaticResyncPeriodFunc(ctx.Options.ResourceQuotaSyncPeriod.Duration), ResyncPeriod: controller.StaticResyncPeriodFunc(ctx.Options.ResourceQuotaSyncPeriod.Duration),
Registry: resourceQuotaRegistry, InformerFactory: ctx.InformerFactory,
ControllerFactory: resourcequotacontroller.NewReplenishmentControllerFactory(ctx.InformerFactory),
ReplenishmentResyncPeriod: ResyncPeriod(&ctx.Options), 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 { if resourceQuotaControllerClient.CoreV1().RESTClient().GetRateLimiter() != nil {
metrics.RegisterMetricAndTrackRateLimiterUsage("resource_quota_controller", resourceQuotaControllerClient.CoreV1().RESTClient().GetRateLimiter()) metrics.RegisterMetricAndTrackRateLimiterUsage("resource_quota_controller", resourceQuotaControllerClient.CoreV1().RESTClient().GetRateLimiter())
} }
go resourcequotacontroller.NewResourceQuotaController( resourceQuotaController, err := resourcequotacontroller.NewResourceQuotaController(resourceQuotaControllerOptions)
resourceQuotaControllerOptions, if err != nil {
).Run(int(ctx.Options.ConcurrentResourceQuotaSyncs), ctx.Stop) 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 return true, nil
} }
......
...@@ -235,7 +235,6 @@ pkg/proxy/util ...@@ -235,7 +235,6 @@ pkg/proxy/util
pkg/proxy/winkernel pkg/proxy/winkernel
pkg/proxy/winuserspace pkg/proxy/winuserspace
pkg/quota/evaluator/core pkg/quota/evaluator/core
pkg/quota/generic
pkg/registry/admissionregistration/externaladmissionhookconfiguration/storage pkg/registry/admissionregistration/externaladmissionhookconfiguration/storage
pkg/registry/admissionregistration/initializerconfiguration/storage pkg/registry/admissionregistration/initializerconfiguration/storage
pkg/registry/admissionregistration/rest pkg/registry/admissionregistration/rest
......
...@@ -10,8 +10,8 @@ go_library( ...@@ -10,8 +10,8 @@ go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"doc.go", "doc.go",
"replenishment_controller.go",
"resource_quota_controller.go", "resource_quota_controller.go",
"resource_quota_monitor.go",
], ],
importpath = "k8s.io/kubernetes/pkg/controller/resourcequota", importpath = "k8s.io/kubernetes/pkg/controller/resourcequota",
deps = [ deps = [
...@@ -20,6 +20,7 @@ go_library( ...@@ -20,6 +20,7 @@ go_library(
"//pkg/controller:go_default_library", "//pkg/controller:go_default_library",
"//pkg/quota:go_default_library", "//pkg/quota:go_default_library",
"//pkg/quota/evaluator/core: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/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
...@@ -27,11 +28,12 @@ go_library( ...@@ -27,11 +28,12 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_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/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels: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/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/clock: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/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait: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:go_default_library",
"//vendor/k8s.io/client-go/informers/core/v1: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", "//vendor/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
...@@ -43,15 +45,12 @@ go_library( ...@@ -43,15 +45,12 @@ go_library(
go_test( go_test(
name = "go_default_test", name = "go_default_test",
srcs = [ srcs = ["resource_quota_controller_test.go"],
"replenishment_controller_test.go",
"resource_quota_controller_test.go",
],
importpath = "k8s.io/kubernetes/pkg/controller/resourcequota", importpath = "k8s.io/kubernetes/pkg/controller/resourcequota",
library = ":go_default_library", library = ":go_default_library",
deps = [ deps = [
"//pkg/api:go_default_library",
"//pkg/controller:go_default_library", "//pkg/controller:go_default_library",
"//pkg/quota:go_default_library",
"//pkg/quota/generic:go_default_library", "//pkg/quota/generic:go_default_library",
"//pkg/quota/install:go_default_library", "//pkg/quota/install:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library",
...@@ -59,12 +58,12 @@ go_test( ...@@ -59,12 +58,12 @@ go_test(
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1: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:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema: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/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/client-go/informers: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/kubernetes/fake:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library", "//vendor/k8s.io/client-go/testing:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
], ],
) )
......
/*
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)
}
/*
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)
}
}
...@@ -53,9 +53,9 @@ type WantsRESTMapper interface { ...@@ -53,9 +53,9 @@ type WantsRESTMapper interface {
SetRESTMapper(meta.RESTMapper) SetRESTMapper(meta.RESTMapper)
} }
// WantsQuotaRegistry defines a function which sets quota registry for admission plugins that need it. // WantsQuotaConfiguration defines a function which sets quota configuration for admission plugins that need it.
type WantsQuotaRegistry interface { type WantsQuotaConfiguration interface {
SetQuotaRegistry(quota.Registry) SetQuotaConfiguration(quota.Configuration)
admission.Validator admission.Validator
} }
...@@ -85,7 +85,7 @@ type PluginInitializer struct { ...@@ -85,7 +85,7 @@ type PluginInitializer struct {
authorizer authorizer.Authorizer authorizer authorizer.Authorizer
cloudConfig []byte cloudConfig []byte
restMapper meta.RESTMapper restMapper meta.RESTMapper
quotaRegistry quota.Registry quotaConfiguration quota.Configuration
serviceResolver webhook.ServiceResolver serviceResolver webhook.ServiceResolver
authenticationInfoResolverWrapper webhook.AuthenticationInfoResolverWrapper authenticationInfoResolverWrapper webhook.AuthenticationInfoResolverWrapper
} }
...@@ -100,7 +100,7 @@ func NewPluginInitializer( ...@@ -100,7 +100,7 @@ func NewPluginInitializer(
sharedInformers informers.SharedInformerFactory, sharedInformers informers.SharedInformerFactory,
cloudConfig []byte, cloudConfig []byte,
restMapper meta.RESTMapper, restMapper meta.RESTMapper,
quotaRegistry quota.Registry, quotaConfiguration quota.Configuration,
authenticationInfoResolverWrapper webhook.AuthenticationInfoResolverWrapper, authenticationInfoResolverWrapper webhook.AuthenticationInfoResolverWrapper,
serviceResolver webhook.ServiceResolver, serviceResolver webhook.ServiceResolver,
) *PluginInitializer { ) *PluginInitializer {
...@@ -109,7 +109,7 @@ func NewPluginInitializer( ...@@ -109,7 +109,7 @@ func NewPluginInitializer(
informers: sharedInformers, informers: sharedInformers,
cloudConfig: cloudConfig, cloudConfig: cloudConfig,
restMapper: restMapper, restMapper: restMapper,
quotaRegistry: quotaRegistry, quotaConfiguration: quotaConfiguration,
authenticationInfoResolverWrapper: authenticationInfoResolverWrapper, authenticationInfoResolverWrapper: authenticationInfoResolverWrapper,
serviceResolver: serviceResolver, serviceResolver: serviceResolver,
} }
...@@ -134,8 +134,8 @@ func (i *PluginInitializer) Initialize(plugin admission.Interface) { ...@@ -134,8 +134,8 @@ func (i *PluginInitializer) Initialize(plugin admission.Interface) {
wants.SetRESTMapper(i.restMapper) wants.SetRESTMapper(i.restMapper)
} }
if wants, ok := plugin.(WantsQuotaRegistry); ok { if wants, ok := plugin.(WantsQuotaConfiguration); ok {
wants.SetQuotaRegistry(i.quotaRegistry) wants.SetQuotaConfiguration(i.quotaConfiguration)
} }
if wants, ok := plugin.(WantsServiceResolver); ok { if wants, ok := plugin.(WantsServiceResolver); ok {
......
...@@ -21,6 +21,7 @@ go_library( ...@@ -21,6 +21,7 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/runtime/schema: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/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library", "//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
], ],
) )
......
...@@ -9,14 +9,10 @@ load( ...@@ -9,14 +9,10 @@ load(
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"configmap.go",
"doc.go", "doc.go",
"persistent_volume_claims.go", "persistent_volume_claims.go",
"pods.go", "pods.go",
"registry.go", "registry.go",
"replication_controllers.go",
"resource_quotas.go",
"secrets.go",
"services.go", "services.go",
], ],
importpath = "k8s.io/kubernetes/pkg/quota/evaluator/core", importpath = "k8s.io/kubernetes/pkg/quota/evaluator/core",
...@@ -32,7 +28,6 @@ go_library( ...@@ -32,7 +28,6 @@ go_library(
"//pkg/quota/generic:go_default_library", "//pkg/quota/generic:go_default_library",
"//vendor/k8s.io/api/core/v1: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/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:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema: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/clock:go_default_library",
...@@ -43,8 +38,6 @@ go_library( ...@@ -43,8 +38,6 @@ go_library(
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library", "//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/features: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/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( ...@@ -60,11 +53,12 @@ go_test(
deps = [ deps = [
"//pkg/api:go_default_library", "//pkg/api:go_default_library",
"//pkg/quota:go_default_library", "//pkg/quota:go_default_library",
"//pkg/quota/generic:go_default_library",
"//pkg/util/node:go_default_library", "//pkg/util/node:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource: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/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/apimachinery/pkg/util/clock:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
], ],
) )
......
/*
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,
}
}
...@@ -22,17 +22,13 @@ import ( ...@@ -22,17 +22,13 @@ import (
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/initialization" "k8s.io/apimachinery/pkg/util/initialization"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature" 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"
"k8s.io/kubernetes/pkg/api/helper" "k8s.io/kubernetes/pkg/api/helper"
k8s_api_v1 "k8s.io/kubernetes/pkg/api/v1" k8s_api_v1 "k8s.io/kubernetes/pkg/api/v1"
...@@ -42,6 +38,9 @@ import ( ...@@ -42,6 +38,9 @@ import (
"k8s.io/kubernetes/pkg/quota/generic" "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. // 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. // for each resouce in this list, it may be refined dynamically based on storage class.
var pvcResources = []api.ResourceName{ var pvcResources = []api.ResourceName{
...@@ -67,34 +66,11 @@ func V1ResourceByStorageClass(storageClass string, resourceName v1.ResourceName) ...@@ -67,34 +66,11 @@ func V1ResourceByStorageClass(storageClass string, resourceName v1.ResourceName)
return v1.ResourceName(string(storageClass + storageClassSuffix + string(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 // 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(f quota.ListerForResourceFunc) quota.Evaluator {
func NewPersistentVolumeClaimEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Evaluator { listFuncByNamespace := generic.ListResourceUsingListerFunc(f, v1.SchemeGroupVersion.WithResource("persistentvolumeclaims"))
listFuncByNamespace := listPersistentVolumeClaimsByNamespaceFuncUsingClient(kubeClient) pvcEvaluator := &pvcEvaluator{listFuncByNamespace: listFuncByNamespace}
if f != nil { return pvcEvaluator
listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, v1.SchemeGroupVersion.WithResource("persistentvolumeclaims"))
}
return &pvcEvaluator{
listFuncByNamespace: listFuncByNamespace,
}
} }
// pvcEvaluator knows how to evaluate quota usage for persistent volume claims // pvcEvaluator knows how to evaluate quota usage for persistent volume claims
...@@ -105,45 +81,13 @@ type pvcEvaluator struct { ...@@ -105,45 +81,13 @@ type pvcEvaluator struct {
// Constraints verifies that all required resources are present on the item. // Constraints verifies that all required resources are present on the item.
func (p *pvcEvaluator) Constraints(required []api.ResourceName, item runtime.Object) error { func (p *pvcEvaluator) Constraints(required []api.ResourceName, item runtime.Object) error {
pvc, ok := item.(*api.PersistentVolumeClaim) // no-op for persistent volume claims
if !ok { return nil
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(), ","))
} }
// GroupKind that this evaluator tracks // GroupResource that this evaluator tracks
func (p *pvcEvaluator) GroupKind() schema.GroupKind { func (p *pvcEvaluator) GroupResource() schema.GroupResource {
return api.Kind("PersistentVolumeClaim") return v1.SchemeGroupVersion.WithResource("persistentvolumeclaims").GroupResource()
} }
// Handles returns true if the evaluator should handle the specified operation. // 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 ...@@ -183,6 +127,12 @@ func (p *pvcEvaluator) Matches(resourceQuota *api.ResourceQuota, item runtime.Ob
func (p *pvcEvaluator) MatchingResources(items []api.ResourceName) []api.ResourceName { func (p *pvcEvaluator) MatchingResources(items []api.ResourceName) []api.ResourceName {
result := []api.ResourceName{} result := []api.ResourceName{}
for _, item := range items { 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) { if quota.Contains(pvcResources, item) {
result = append(result, item) result = append(result, item)
continue continue
...@@ -208,7 +158,8 @@ func (p *pvcEvaluator) Usage(item runtime.Object) (api.ResourceList, error) { ...@@ -208,7 +158,8 @@ func (p *pvcEvaluator) Usage(item runtime.Object) (api.ResourceList, error) {
} }
// charge for claim // 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 utilfeature.DefaultFeatureGate.Enabled(features.Initializers) {
if !initialization.IsInitialized(pvc.Initializers) { if !initialization.IsInitialized(pvc.Initializers) {
// Only charge pvc count for uninitialized pvc. // Only charge pvc count for uninitialized pvc.
...@@ -218,7 +169,7 @@ func (p *pvcEvaluator) Usage(item runtime.Object) (api.ResourceList, error) { ...@@ -218,7 +169,7 @@ func (p *pvcEvaluator) Usage(item runtime.Object) (api.ResourceList, error) {
storageClassRef := helper.GetPersistentVolumeClaimClass(pvc) storageClassRef := helper.GetPersistentVolumeClaimClass(pvc)
if len(storageClassRef) > 0 { if len(storageClassRef) > 0 {
storageClassClaim := api.ResourceName(storageClassRef + storageClassSuffix + string(api.ResourcePersistentVolumeClaims)) storageClassClaim := api.ResourceName(storageClassRef + storageClassSuffix + string(api.ResourcePersistentVolumeClaims))
result[storageClassClaim] = resource.MustParse("1") result[storageClassClaim] = *(resource.NewQuantity(1, resource.DecimalSI))
} }
// charge for storage // charge for storage
......
...@@ -21,9 +21,10 @@ import ( ...@@ -21,9 +21,10 @@ import (
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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/api"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
) )
func testVolumeClaim(name string, namespace string, spec api.PersistentVolumeClaimSpec) *api.PersistentVolumeClaim { func testVolumeClaim(name string, namespace string, spec api.PersistentVolumeClaimSpec) *api.PersistentVolumeClaim {
...@@ -33,168 +34,6 @@ func testVolumeClaim(name string, namespace string, spec api.PersistentVolumeCla ...@@ -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) { func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) {
classGold := "gold" classGold := "gold"
validClaim := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{ validClaim := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
...@@ -237,8 +76,7 @@ func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) { ...@@ -237,8 +76,7 @@ func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) {
StorageClassName: &classGold, StorageClassName: &classGold,
}) })
kubeClient := fake.NewSimpleClientset() evaluator := NewPersistentVolumeClaimEvaluator(nil)
evaluator := NewPersistentVolumeClaimEvaluator(kubeClient, nil)
testCases := map[string]struct { testCases := map[string]struct {
pvc *api.PersistentVolumeClaim pvc *api.PersistentVolumeClaim
usage api.ResourceList usage api.ResourceList
...@@ -246,17 +84,19 @@ func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) { ...@@ -246,17 +84,19 @@ func TestPersistentVolumeClaimEvaluatorUsage(t *testing.T) {
"pvc-usage": { "pvc-usage": {
pvc: validClaim, pvc: validClaim,
usage: api.ResourceList{ usage: api.ResourceList{
api.ResourceRequestsStorage: resource.MustParse("10Gi"), api.ResourceRequestsStorage: resource.MustParse("10Gi"),
api.ResourcePersistentVolumeClaims: resource.MustParse("1"), api.ResourcePersistentVolumeClaims: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "persistentvolumeclaims"}): resource.MustParse("1"),
}, },
}, },
"pvc-usage-by-class": { "pvc-usage-by-class": {
pvc: validClaimByStorageClass, pvc: validClaimByStorageClass,
usage: api.ResourceList{ usage: api.ResourceList{
api.ResourceRequestsStorage: resource.MustParse("10Gi"), api.ResourceRequestsStorage: resource.MustParse("10Gi"),
api.ResourcePersistentVolumeClaims: resource.MustParse("1"), api.ResourcePersistentVolumeClaims: resource.MustParse("1"),
ResourceByStorageClass(classGold, api.ResourceRequestsStorage): resource.MustParse("10Gi"), ResourceByStorageClass(classGold, api.ResourceRequestsStorage): resource.MustParse("10Gi"),
ResourceByStorageClass(classGold, api.ResourcePersistentVolumeClaims): resource.MustParse("1"), ResourceByStorageClass(classGold, api.ResourcePersistentVolumeClaims): resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "persistentvolumeclaims"}): resource.MustParse("1"),
}, },
}, },
} }
......
...@@ -23,20 +23,14 @@ import ( ...@@ -23,20 +23,14 @@ import (
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/clock"
"k8s.io/apimachinery/pkg/util/initialization"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/admission" "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"
"k8s.io/kubernetes/pkg/api/helper/qos" "k8s.io/kubernetes/pkg/api/helper/qos"
k8s_api_v1 "k8s.io/kubernetes/pkg/api/v1" k8s_api_v1 "k8s.io/kubernetes/pkg/api/v1"
...@@ -46,8 +40,12 @@ import ( ...@@ -46,8 +40,12 @@ import (
"k8s.io/kubernetes/pkg/quota/generic" "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. // podResources are the set of resources managed by quota associated with pods.
var podResources = []api.ResourceName{ var podResources = []api.ResourceName{
podObjectCountName,
api.ResourceCPU, api.ResourceCPU,
api.ResourceMemory, api.ResourceMemory,
api.ResourceEphemeralStorage, api.ResourceEphemeralStorage,
...@@ -60,35 +58,24 @@ var podResources = []api.ResourceName{ ...@@ -60,35 +58,24 @@ var podResources = []api.ResourceName{
api.ResourcePods, api.ResourcePods,
} }
// listPodsByNamespaceFuncUsingClient returns a pod listing function based on the provided client. // NOTE: it was a mistake, but if a quota tracks cpu or memory related resources,
func listPodsByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.ListFuncByNamespace { // the incoming pod is required to have those values set. we should not repeat
// TODO: ideally, we could pass dynamic client pool down into this code, and have one way of doing this. // this mistake for other future resources (gpus, ephemeral-storage,etc).
// unfortunately, dynamic client works with Unstructured objects, and when we calculate Usage, we require // do not add more resources to this list!
// structured objects. var validationSet = sets.NewString(
return func(namespace string, options metav1.ListOptions) ([]runtime.Object, error) { string(api.ResourceCPU),
itemList, err := kubeClient.CoreV1().Pods(namespace).List(options) string(api.ResourceMemory),
if err != nil { string(api.ResourceRequestsCPU),
return nil, err string(api.ResourceRequestsMemory),
} string(api.ResourceLimitsCPU),
results := make([]runtime.Object, 0, len(itemList.Items)) string(api.ResourceLimitsMemory),
for i := range itemList.Items { )
results = append(results, &itemList.Items[i])
}
return results, nil
}
}
// NewPodEvaluator returns an evaluator that can evaluate pods // NewPodEvaluator returns an evaluator that can evaluate pods
// if the specified shared informer factory is not nil, evaluator may use it to support listing functions. func NewPodEvaluator(f quota.ListerForResourceFunc, clock clock.Clock) quota.Evaluator {
func NewPodEvaluator(kubeClient clientset.Interface, f informers.SharedInformerFactory, clock clock.Clock) quota.Evaluator { listFuncByNamespace := generic.ListResourceUsingListerFunc(f, v1.SchemeGroupVersion.WithResource("pods"))
listFuncByNamespace := listPodsByNamespaceFuncUsingClient(kubeClient) podEvaluator := &podEvaluator{listFuncByNamespace: listFuncByNamespace, clock: clock}
if f != nil { return podEvaluator
listFuncByNamespace = generic.ListResourceUsingInformerFunc(f, v1.SchemeGroupVersion.WithResource("pods"))
}
return &podEvaluator{
listFuncByNamespace: listFuncByNamespace,
clock: clock,
}
} }
// podEvaluator knows how to measure usage of pods. // podEvaluator knows how to measure usage of pods.
...@@ -110,6 +97,7 @@ func (p *podEvaluator) Constraints(required []api.ResourceName, item runtime.Obj ...@@ -110,6 +97,7 @@ func (p *podEvaluator) Constraints(required []api.ResourceName, item runtime.Obj
// Pod level resources are often set during admission control // Pod level resources are often set during admission control
// As a consequence, we want to verify that resources are valid prior // As a consequence, we want to verify that resources are valid prior
// to ever charging quota prematurely in case they are not. // 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{} allErrs := field.ErrorList{}
fldPath := field.NewPath("spec").Child("containers") fldPath := field.NewPath("spec").Child("containers")
for i, ctr := range pod.Spec.Containers { for i, ctr := range pod.Spec.Containers {
...@@ -123,10 +111,11 @@ func (p *podEvaluator) Constraints(required []api.ResourceName, item runtime.Obj ...@@ -123,10 +111,11 @@ func (p *podEvaluator) Constraints(required []api.ResourceName, item runtime.Obj
return allErrs.ToAggregate() return allErrs.ToAggregate()
} }
// TODO: fix this when we have pod level resource requirements // BACKWARD COMPATIBILITY REQUIREMENT: if we quota cpu or memory, then each container
// since we do not yet pod level requests/limits, we need to ensure each // must make an explicit request for the resource. this was a mistake. it coupled
// container makes an explict request or limit for a quota tracked resource // validation with resource counting, but we did this before QoS was even defined.
requiredSet := quota.ToSet(required) // let's not make that mistake again with other resources now that QoS is defined.
requiredSet := quota.ToSet(required).Intersection(validationSet)
missingSet := sets.NewString() missingSet := sets.NewString()
for i := range pod.Spec.Containers { for i := range pod.Spec.Containers {
enforcePodContainerConstraints(&pod.Spec.Containers[i], requiredSet, missingSet) enforcePodContainerConstraints(&pod.Spec.Containers[i], requiredSet, missingSet)
...@@ -140,9 +129,9 @@ func (p *podEvaluator) Constraints(required []api.ResourceName, item runtime.Obj ...@@ -140,9 +129,9 @@ func (p *podEvaluator) Constraints(required []api.ResourceName, item runtime.Obj
return fmt.Errorf("must specify %s", strings.Join(missingSet.List(), ",")) return fmt.Errorf("must specify %s", strings.Join(missingSet.List(), ","))
} }
// GroupKind that this evaluator tracks // GroupResource that this evaluator tracks
func (p *podEvaluator) GroupKind() schema.GroupKind { func (p *podEvaluator) GroupResource() schema.GroupResource {
return api.Kind("Pod") return v1.SchemeGroupVersion.WithResource("pods").GroupResource()
} }
// Handles returns true if the evaluator should handle the specified attributes. // Handles returns true if the evaluator should handle the specified attributes.
...@@ -190,7 +179,7 @@ var _ quota.Evaluator = &podEvaluator{} ...@@ -190,7 +179,7 @@ var _ quota.Evaluator = &podEvaluator{}
func enforcePodContainerConstraints(container *api.Container, requiredSet, missingSet sets.String) { func enforcePodContainerConstraints(container *api.Container, requiredSet, missingSet sets.String) {
requests := container.Resources.Requests requests := container.Resources.Requests
limits := container.Resources.Limits limits := container.Resources.Limits
containerUsage := podUsageHelper(requests, limits) containerUsage := podComputeUsageHelper(requests, limits)
containerSet := quota.ToSet(quota.ResourceNames(containerUsage)) containerSet := quota.ToSet(quota.ResourceNames(containerUsage))
if !containerSet.Equal(requiredSet) { if !containerSet.Equal(requiredSet) {
difference := requiredSet.Difference(containerSet) difference := requiredSet.Difference(containerSet)
...@@ -198,8 +187,8 @@ func enforcePodContainerConstraints(container *api.Container, requiredSet, missi ...@@ -198,8 +187,8 @@ func enforcePodContainerConstraints(container *api.Container, requiredSet, missi
} }
} }
// podUsageHelper can summarize the pod quota usage based on requests and limits // podComputeUsageHelper can summarize the pod compute quota usage based on requests and limits
func podUsageHelper(requests api.ResourceList, limits api.ResourceList) api.ResourceList { func podComputeUsageHelper(requests api.ResourceList, limits api.ResourceList) api.ResourceList {
result := api.ResourceList{} result := api.ResourceList{}
result[api.ResourcePods] = resource.MustParse("1") result[api.ResourcePods] = resource.MustParse("1")
if request, found := requests[api.ResourceCPU]; found { if request, found := requests[api.ResourceCPU]; found {
...@@ -269,18 +258,21 @@ func PodUsageFunc(obj runtime.Object, clock clock.Clock) (api.ResourceList, erro ...@@ -269,18 +258,21 @@ func PodUsageFunc(obj runtime.Object, clock clock.Clock) (api.ResourceList, erro
if err != nil { if err != nil {
return api.ResourceList{}, err return api.ResourceList{}, err
} }
// by convention, we do not quota pods that have reached end-of life
if !QuotaPod(pod, clock) { // always quota the object count (even if the pod is end of life)
return api.ResourceList{}, nil // 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)),
} }
// Only charge pod count for uninitialized pod.
if utilfeature.DefaultFeatureGate.Enabled(features.Initializers) { // by convention, we do not quota compute resources that have reached end-of life
if !initialization.IsInitialized(pod.Initializers) { // note: the "pods" resource is considered a compute resource since it is tied to life-cycle.
result := api.ResourceList{} if !QuotaPod(pod, clock) {
result[api.ResourcePods] = resource.MustParse("1") return result, nil
return result, nil
}
} }
requests := api.ResourceList{} requests := api.ResourceList{}
limits := api.ResourceList{} limits := api.ResourceList{}
// TODO: ideally, we have pod level requests and limits in the future. // 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 ...@@ -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) 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 { func isBestEffort(pod *api.Pod) bool {
......
...@@ -22,10 +22,11 @@ import ( ...@@ -22,10 +22,11 @@ import (
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/clock"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
"k8s.io/kubernetes/pkg/util/node" "k8s.io/kubernetes/pkg/util/node"
) )
...@@ -90,8 +91,7 @@ func TestPodConstraintsFunc(t *testing.T) { ...@@ -90,8 +91,7 @@ func TestPodConstraintsFunc(t *testing.T) {
err: `must specify memory`, err: `must specify memory`,
}, },
} }
kubeClient := fake.NewSimpleClientset() evaluator := NewPodEvaluator(nil, clock.RealClock{})
evaluator := NewPodEvaluator(kubeClient, nil, clock.RealClock{})
for testName, test := range testCases { for testName, test := range testCases {
err := evaluator.Constraints(test.required, test.pod) err := evaluator.Constraints(test.required, test.pod)
switch { switch {
...@@ -104,9 +104,8 @@ func TestPodConstraintsFunc(t *testing.T) { ...@@ -104,9 +104,8 @@ func TestPodConstraintsFunc(t *testing.T) {
} }
func TestPodEvaluatorUsage(t *testing.T) { func TestPodEvaluatorUsage(t *testing.T) {
kubeClient := fake.NewSimpleClientset()
fakeClock := clock.NewFakeClock(time.Now()) fakeClock := clock.NewFakeClock(time.Now())
evaluator := NewPodEvaluator(kubeClient, nil, fakeClock) evaluator := NewPodEvaluator(nil, fakeClock)
// fields use to simulate a pod undergoing termination // fields use to simulate a pod undergoing termination
// note: we set the deletion time in the past // note: we set the deletion time in the past
...@@ -135,6 +134,7 @@ func TestPodEvaluatorUsage(t *testing.T) { ...@@ -135,6 +134,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceLimitsCPU: resource.MustParse("2m"), api.ResourceLimitsCPU: resource.MustParse("2m"),
api.ResourcePods: resource.MustParse("1"), api.ResourcePods: resource.MustParse("1"),
api.ResourceCPU: resource.MustParse("1m"), api.ResourceCPU: resource.MustParse("1m"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
}, },
}, },
"init container MEM": { "init container MEM": {
...@@ -153,6 +153,7 @@ func TestPodEvaluatorUsage(t *testing.T) { ...@@ -153,6 +153,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceLimitsMemory: resource.MustParse("2m"), api.ResourceLimitsMemory: resource.MustParse("2m"),
api.ResourcePods: resource.MustParse("1"), api.ResourcePods: resource.MustParse("1"),
api.ResourceMemory: resource.MustParse("1m"), api.ResourceMemory: resource.MustParse("1m"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
}, },
}, },
"init container local ephemeral storage": { "init container local ephemeral storage": {
...@@ -171,6 +172,7 @@ func TestPodEvaluatorUsage(t *testing.T) { ...@@ -171,6 +172,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceRequestsEphemeralStorage: resource.MustParse("32Mi"), api.ResourceRequestsEphemeralStorage: resource.MustParse("32Mi"),
api.ResourceLimitsEphemeralStorage: resource.MustParse("64Mi"), api.ResourceLimitsEphemeralStorage: resource.MustParse("64Mi"),
api.ResourcePods: resource.MustParse("1"), api.ResourcePods: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
}, },
}, },
"container CPU": { "container CPU": {
...@@ -189,6 +191,7 @@ func TestPodEvaluatorUsage(t *testing.T) { ...@@ -189,6 +191,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceLimitsCPU: resource.MustParse("2m"), api.ResourceLimitsCPU: resource.MustParse("2m"),
api.ResourcePods: resource.MustParse("1"), api.ResourcePods: resource.MustParse("1"),
api.ResourceCPU: resource.MustParse("1m"), api.ResourceCPU: resource.MustParse("1m"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
}, },
}, },
"container MEM": { "container MEM": {
...@@ -207,6 +210,7 @@ func TestPodEvaluatorUsage(t *testing.T) { ...@@ -207,6 +210,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceLimitsMemory: resource.MustParse("2m"), api.ResourceLimitsMemory: resource.MustParse("2m"),
api.ResourcePods: resource.MustParse("1"), api.ResourcePods: resource.MustParse("1"),
api.ResourceMemory: resource.MustParse("1m"), api.ResourceMemory: resource.MustParse("1m"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
}, },
}, },
"container local ephemeral storage": { "container local ephemeral storage": {
...@@ -225,6 +229,7 @@ func TestPodEvaluatorUsage(t *testing.T) { ...@@ -225,6 +229,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceRequestsEphemeralStorage: resource.MustParse("32Mi"), api.ResourceRequestsEphemeralStorage: resource.MustParse("32Mi"),
api.ResourceLimitsEphemeralStorage: resource.MustParse("64Mi"), api.ResourceLimitsEphemeralStorage: resource.MustParse("64Mi"),
api.ResourcePods: resource.MustParse("1"), api.ResourcePods: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
}, },
}, },
"init container maximums override sum of containers": { "init container maximums override sum of containers": {
...@@ -292,6 +297,7 @@ func TestPodEvaluatorUsage(t *testing.T) { ...@@ -292,6 +297,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourcePods: resource.MustParse("1"), api.ResourcePods: resource.MustParse("1"),
api.ResourceCPU: resource.MustParse("4"), api.ResourceCPU: resource.MustParse("4"),
api.ResourceMemory: resource.MustParse("100M"), api.ResourceMemory: resource.MustParse("100M"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
}, },
}, },
"pod deletion timestamp exceeded": { "pod deletion timestamp exceeded": {
...@@ -321,7 +327,9 @@ func TestPodEvaluatorUsage(t *testing.T) { ...@@ -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 deletion timestamp not exceeded": {
pod: &api.Pod{ pod: &api.Pod{
...@@ -352,6 +360,7 @@ func TestPodEvaluatorUsage(t *testing.T) { ...@@ -352,6 +360,7 @@ func TestPodEvaluatorUsage(t *testing.T) {
api.ResourceLimitsCPU: resource.MustParse("2"), api.ResourceLimitsCPU: resource.MustParse("2"),
api.ResourcePods: resource.MustParse("1"), api.ResourcePods: resource.MustParse("1"),
api.ResourceCPU: resource.MustParse("1"), api.ResourceCPU: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"),
}, },
}, },
} }
......
...@@ -17,33 +17,34 @@ limitations under the License. ...@@ -17,33 +17,34 @@ limitations under the License.
package core package core
import ( import (
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/clock"
"k8s.io/client-go/informers" "k8s.io/kubernetes/pkg/api"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic" "k8s.io/kubernetes/pkg/quota/generic"
) )
// NewRegistry returns a registry that knows how to deal with core kubernetes resources // legacyObjectCountAliases are what we used to do simple object counting quota with mapped to alias
// If an informer factory is provided, evaluators will use them. var legacyObjectCountAliases = map[schema.GroupVersionResource]api.ResourceName{
func NewRegistry(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Registry { v1.SchemeGroupVersion.WithResource("configmaps"): api.ResourceConfigMaps,
pod := NewPodEvaluator(kubeClient, f, clock.RealClock{}) v1.SchemeGroupVersion.WithResource("resourcequotas"): api.ResourceQuotas,
service := NewServiceEvaluator(kubeClient, f) v1.SchemeGroupVersion.WithResource("replicationcontrollers"): api.ResourceReplicationControllers,
replicationController := NewReplicationControllerEvaluator(kubeClient, f) v1.SchemeGroupVersion.WithResource("secrets"): api.ResourceSecrets,
resourceQuota := NewResourceQuotaEvaluator(kubeClient, f) }
secret := NewSecretEvaluator(kubeClient, f)
configMap := NewConfigMapEvaluator(kubeClient, f) // NewEvaluators returns the list of static evaluators that manage more than counts
persistentVolumeClaim := NewPersistentVolumeClaimEvaluator(kubeClient, f) func NewEvaluators(f quota.ListerForResourceFunc) []quota.Evaluator {
return &generic.GenericRegistry{ // these evaluators have special logic
InternalEvaluators: map[schema.GroupKind]quota.Evaluator{ result := []quota.Evaluator{
pod.GroupKind(): pod, NewPodEvaluator(f, clock.RealClock{}),
service.GroupKind(): service, NewServiceEvaluator(f),
replicationController.GroupKind(): replicationController, NewPersistentVolumeClaimEvaluator(f),
secret.GroupKind(): secret, }
configMap.GroupKind(): configMap, // these evaluators require an alias for backwards compatibility
resourceQuota.GroupKind(): resourceQuota, for gvr, alias := range legacyObjectCountAliases {
persistentVolumeClaim.GroupKind(): persistentVolumeClaim, result = append(result,
}, generic.NewObjectCountEvaluator(false, gvr.GroupResource(), generic.ListResourceUsingListerFunc(f, gvr), alias))
} }
return result
} }
/*
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,
}
}
/*
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,
}
}
/*
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,
}
}
...@@ -18,58 +18,34 @@ package core ...@@ -18,58 +18,34 @@ package core
import ( import (
"fmt" "fmt"
"strings"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
k8s_api_v1 "k8s.io/kubernetes/pkg/api/v1" k8s_api_v1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic" "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. // serviceResources are the set of resources managed by quota associated with services.
var serviceResources = []api.ResourceName{ var serviceResources = []api.ResourceName{
serviceObjectCountName,
api.ResourceServices, api.ResourceServices,
api.ResourceServicesNodePorts, api.ResourceServicesNodePorts,
api.ResourceServicesLoadBalancers, api.ResourceServicesLoadBalancers,
} }
// listServicesByNamespaceFuncUsingClient returns a service listing function based on the provided client. // NewServiceEvaluator returns an evaluator that can evaluate services.
func listServicesByNamespaceFuncUsingClient(kubeClient clientset.Interface) generic.ListFuncByNamespace { func NewServiceEvaluator(f quota.ListerForResourceFunc) quota.Evaluator {
// TODO: ideally, we could pass dynamic client pool down into this code, and have one way of doing this. listFuncByNamespace := generic.ListResourceUsingListerFunc(f, v1.SchemeGroupVersion.WithResource("services"))
// unfortunately, dynamic client works with Unstructured objects, and when we calculate Usage, we require serviceEvaluator := &serviceEvaluator{listFuncByNamespace: listFuncByNamespace}
// structured objects. return serviceEvaluator
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,
}
} }
// serviceEvaluator knows how to measure usage for services. // serviceEvaluator knows how to measure usage for services.
...@@ -80,31 +56,13 @@ type serviceEvaluator struct { ...@@ -80,31 +56,13 @@ type serviceEvaluator struct {
// Constraints verifies that all required resources are present on the item // Constraints verifies that all required resources are present on the item
func (p *serviceEvaluator) Constraints(required []api.ResourceName, item runtime.Object) error { func (p *serviceEvaluator) Constraints(required []api.ResourceName, item runtime.Object) error {
service, ok := item.(*api.Service) // this is a no-op for services
if !ok { return nil
return fmt.Errorf("unexpected input object %v", item)
}
requiredSet := quota.ToSet(required)
missingSet := sets.NewString()
serviceUsage, err := p.Usage(service)
if err != nil {
return err
}
serviceSet := quota.ToSet(quota.ResourceNames(serviceUsage))
if diff := requiredSet.Difference(serviceSet); len(diff) > 0 {
missingSet.Insert(diff.List()...)
}
if len(missingSet) == 0 {
return nil
}
return fmt.Errorf("must specify %s", strings.Join(missingSet.List(), ","))
} }
// GroupKind that this evaluator tracks // GroupResource that this evaluator tracks
func (p *serviceEvaluator) GroupKind() schema.GroupKind { func (p *serviceEvaluator) GroupResource() schema.GroupResource {
return api.Kind("Service") return v1.SchemeGroupVersion.WithResource("services").GroupResource()
} }
// Handles returns true of the evaluator should handle the specified operation. // 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) ...@@ -149,6 +107,7 @@ func (p *serviceEvaluator) Usage(item runtime.Object) (api.ResourceList, error)
} }
ports := len(svc.Spec.Ports) ports := len(svc.Spec.Ports)
// default service usage // default service usage
result[serviceObjectCountName] = *(resource.NewQuantity(1, resource.DecimalSI))
result[api.ResourceServices] = *(resource.NewQuantity(1, resource.DecimalSI)) result[api.ResourceServices] = *(resource.NewQuantity(1, resource.DecimalSI))
result[api.ResourceServicesLoadBalancers] = resource.Quantity{Format: resource.DecimalSI} result[api.ResourceServicesLoadBalancers] = resource.Quantity{Format: resource.DecimalSI}
result[api.ResourceServicesNodePorts] = resource.Quantity{Format: resource.DecimalSI} result[api.ResourceServicesNodePorts] = resource.Quantity{Format: resource.DecimalSI}
......
...@@ -20,14 +20,14 @@ import ( ...@@ -20,14 +20,14 @@ import (
"testing" "testing"
"k8s.io/apimachinery/pkg/api/resource" "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/api"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/generic"
) )
func TestServiceEvaluatorMatchesResources(t *testing.T) { func TestServiceEvaluatorMatchesResources(t *testing.T) {
kubeClient := fake.NewSimpleClientset() evaluator := NewServiceEvaluator(nil)
evaluator := NewServiceEvaluator(kubeClient, nil)
// we give a lot of resources // we give a lot of resources
input := []api.ResourceName{ input := []api.ResourceName{
api.ResourceConfigMaps, api.ResourceConfigMaps,
...@@ -49,8 +49,7 @@ func TestServiceEvaluatorMatchesResources(t *testing.T) { ...@@ -49,8 +49,7 @@ func TestServiceEvaluatorMatchesResources(t *testing.T) {
} }
func TestServiceEvaluatorUsage(t *testing.T) { func TestServiceEvaluatorUsage(t *testing.T) {
kubeClient := fake.NewSimpleClientset() evaluator := NewServiceEvaluator(nil)
evaluator := NewServiceEvaluator(kubeClient, nil)
testCases := map[string]struct { testCases := map[string]struct {
service *api.Service service *api.Service
usage api.ResourceList usage api.ResourceList
...@@ -65,6 +64,7 @@ func TestServiceEvaluatorUsage(t *testing.T) { ...@@ -65,6 +64,7 @@ func TestServiceEvaluatorUsage(t *testing.T) {
api.ResourceServicesNodePorts: resource.MustParse("0"), api.ResourceServicesNodePorts: resource.MustParse("0"),
api.ResourceServicesLoadBalancers: resource.MustParse("1"), api.ResourceServicesLoadBalancers: resource.MustParse("1"),
api.ResourceServices: resource.MustParse("1"), api.ResourceServices: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "services"}): resource.MustParse("1"),
}, },
}, },
"loadbalancer_ports": { "loadbalancer_ports": {
...@@ -82,6 +82,7 @@ func TestServiceEvaluatorUsage(t *testing.T) { ...@@ -82,6 +82,7 @@ func TestServiceEvaluatorUsage(t *testing.T) {
api.ResourceServicesNodePorts: resource.MustParse("1"), api.ResourceServicesNodePorts: resource.MustParse("1"),
api.ResourceServicesLoadBalancers: resource.MustParse("1"), api.ResourceServicesLoadBalancers: resource.MustParse("1"),
api.ResourceServices: resource.MustParse("1"), api.ResourceServices: resource.MustParse("1"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "services"}): resource.MustParse("1"),
}, },
}, },
"clusterip": { "clusterip": {
...@@ -91,9 +92,10 @@ func TestServiceEvaluatorUsage(t *testing.T) { ...@@ -91,9 +92,10 @@ func TestServiceEvaluatorUsage(t *testing.T) {
}, },
}, },
usage: api.ResourceList{ usage: api.ResourceList{
api.ResourceServices: resource.MustParse("1"), api.ResourceServices: resource.MustParse("1"),
api.ResourceServicesNodePorts: resource.MustParse("0"), api.ResourceServicesNodePorts: resource.MustParse("0"),
api.ResourceServicesLoadBalancers: resource.MustParse("0"), api.ResourceServicesLoadBalancers: resource.MustParse("0"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "services"}): resource.MustParse("1"),
}, },
}, },
"nodeports": { "nodeports": {
...@@ -108,9 +110,10 @@ func TestServiceEvaluatorUsage(t *testing.T) { ...@@ -108,9 +110,10 @@ func TestServiceEvaluatorUsage(t *testing.T) {
}, },
}, },
usage: api.ResourceList{ usage: api.ResourceList{
api.ResourceServices: resource.MustParse("1"), api.ResourceServices: resource.MustParse("1"),
api.ResourceServicesNodePorts: resource.MustParse("1"), api.ResourceServicesNodePorts: resource.MustParse("1"),
api.ResourceServicesLoadBalancers: resource.MustParse("0"), api.ResourceServicesLoadBalancers: resource.MustParse("0"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "services"}): resource.MustParse("1"),
}, },
}, },
"multi-nodeports": { "multi-nodeports": {
...@@ -128,9 +131,10 @@ func TestServiceEvaluatorUsage(t *testing.T) { ...@@ -128,9 +131,10 @@ func TestServiceEvaluatorUsage(t *testing.T) {
}, },
}, },
usage: api.ResourceList{ usage: api.ResourceList{
api.ResourceServices: resource.MustParse("1"), api.ResourceServices: resource.MustParse("1"),
api.ResourceServicesNodePorts: resource.MustParse("2"), api.ResourceServicesNodePorts: resource.MustParse("2"),
api.ResourceServicesLoadBalancers: resource.MustParse("0"), api.ResourceServicesLoadBalancers: resource.MustParse("0"),
generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "services"}): resource.MustParse("1"),
}, },
}, },
} }
...@@ -198,8 +202,7 @@ func TestServiceConstraintsFunc(t *testing.T) { ...@@ -198,8 +202,7 @@ func TestServiceConstraintsFunc(t *testing.T) {
}, },
} }
kubeClient := fake.NewSimpleClientset() evaluator := NewServiceEvaluator(nil)
evaluator := NewServiceEvaluator(kubeClient, nil)
for testName, test := range testCases { for testName, test := range testCases {
err := evaluator.Constraints(test.required, test.service) err := evaluator.Constraints(test.required, test.service)
switch { switch {
......
...@@ -8,6 +8,7 @@ load( ...@@ -8,6 +8,7 @@ load(
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"configuration.go",
"evaluator.go", "evaluator.go",
"registry.go", "registry.go",
], ],
...@@ -16,12 +17,12 @@ go_library( ...@@ -16,12 +17,12 @@ go_library(
"//pkg/api:go_default_library", "//pkg/api:go_default_library",
"//pkg/quota:go_default_library", "//pkg/quota:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource: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/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime: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/runtime/schema:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission: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/informers:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
], ],
) )
......
/*
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
}
...@@ -20,33 +20,51 @@ import ( ...@@ -20,33 +20,51 @@ import (
"fmt" "fmt"
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
) )
// ListResourceUsingInformerFunc returns a listing function based on the shared informer factory for the specified resource. // InformerForResourceFunc knows how to provision an informer
func ListResourceUsingInformerFunc(f informers.SharedInformerFactory, resource schema.GroupVersionResource) ListFuncByNamespace { type InformerForResourceFunc func(schema.GroupVersionResource) (informers.GenericInformer, error)
return func(namespace string, options metav1.ListOptions) ([]runtime.Object, error) {
labelSelector, err := labels.Parse(options.LabelSelector) // 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 { if err != nil {
return nil, err return nil, err
} }
informer, err := f.ForResource(resource) 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 { if err != nil {
return nil, err return nil, err
} }
return informer.Lister().ByNamespace(namespace).List(labelSelector) 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 // 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 // MatchesScopeFunc knows how to evaluate if an object matches a scope
type MatchesScopeFunc func(scope api.ResourceQuotaScope, object runtime.Object) (bool, error) type MatchesScopeFunc func(scope api.ResourceQuotaScope, object runtime.Object) (bool, error)
...@@ -91,9 +109,7 @@ func CalculateUsageStats(options quota.UsageStatsOptions, ...@@ -91,9 +109,7 @@ func CalculateUsageStats(options quota.UsageStatsOptions,
for _, resourceName := range options.Resources { for _, resourceName := range options.Resources {
result.Used[resourceName] = resource.Quantity{Format: resource.DecimalSI} result.Used[resourceName] = resource.Quantity{Format: resource.DecimalSI}
} }
items, err := listFunc(options.Namespace, metav1.ListOptions{ items, err := listFunc(options.Namespace)
LabelSelector: labels.Everything().String(),
})
if err != nil { if err != nil {
return result, fmt.Errorf("failed to list content: %v", err) return result, fmt.Errorf("failed to list content: %v", err)
} }
...@@ -121,63 +137,86 @@ func CalculateUsageStats(options quota.UsageStatsOptions, ...@@ -121,63 +137,86 @@ func CalculateUsageStats(options quota.UsageStatsOptions,
return result, nil 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 // that associates usage of the specified resource based on the number of items
// returned by the specified listing function. // returned by the specified listing function.
type ObjectCountEvaluator struct { type objectCountEvaluator struct {
// AllowCreateOnUpdate if true will ensure the evaluator tracks create // allowCreateOnUpdate if true will ensure the evaluator tracks create
// and update operations. // and update operations.
AllowCreateOnUpdate bool allowCreateOnUpdate bool
// GroupKind that this evaluator tracks. // GroupResource that this evaluator tracks.
InternalGroupKind schema.GroupKind // It is used to construct a generic object count quota name
groupResource schema.GroupResource
// A function that knows how to list resources by namespace. // A function that knows how to list resources by namespace.
// TODO move to dynamic client in future // TODO move to dynamic client in future
ListFuncByNamespace ListFuncByNamespace listFuncByNamespace ListFuncByNamespace
// Name associated with this resource in the quota. // Names associated with this resource in the quota for generic counting.
ResourceName api.ResourceName resourceNames []api.ResourceName
} }
// Constraints returns an error if the configured resource name is not in the required set. // 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 { func (o *objectCountEvaluator) Constraints(required []api.ResourceName, item runtime.Object) error {
if !quota.Contains(required, o.ResourceName) { // no-op for object counting
return fmt.Errorf("missing %s", o.ResourceName)
}
return nil 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. // 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() 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 // 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) return Matches(resourceQuota, item, o.MatchingResources, MatchesNoScopeFunc)
} }
// MatchingResources takes the input specified list of resources and returns the set of resources it matches. // 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 { func (o *objectCountEvaluator) MatchingResources(input []api.ResourceName) []api.ResourceName {
return quota.Intersection(input, []api.ResourceName{o.ResourceName}) return quota.Intersection(input, o.resourceNames)
} }
// Usage returns the resource usage for the specified object // 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) quantity := resource.NewQuantity(1, resource.DecimalSI)
return api.ResourceList{ resourceList := api.ResourceList{}
o.ResourceName: *quantity, for _, resourceName := range o.resourceNames {
}, nil 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. // UsageStats calculates aggregate usage for the object.
func (o *ObjectCountEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) { func (o *objectCountEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) {
return CalculateUsageStats(options, o.ListFuncByNamespace, MatchesNoScopeFunc, o.Usage) return CalculateUsageStats(options, o.listFuncByNamespace, MatchesNoScopeFunc, o.Usage)
} }
// Verify implementation of interface at compile time. // 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,
}
}
...@@ -17,20 +17,65 @@ limitations under the License. ...@@ -17,20 +17,65 @@ limitations under the License.
package generic package generic
import ( import (
"sync"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
) )
// Ensure it implements the required interface // implements a basic registry
var _ quota.Registry = &GenericRegistry{} type simpleRegistry struct {
lock sync.RWMutex
// evaluators tracked by the registry
evaluators map[schema.GroupResource]quota.Evaluator
}
// 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)
}
// GenericRegistry implements Registry // evaluatorsByGroupResource converts a list of evaluators to a map by group resource.
type GenericRegistry struct { func evaluatorsByGroupResource(items []quota.Evaluator) map[schema.GroupResource]quota.Evaluator {
// internal evaluators by group kind result := map[schema.GroupResource]quota.Evaluator{}
InternalEvaluators map[schema.GroupKind]quota.Evaluator for _, item := range items {
result[item.GroupResource()] = item
}
return result
} }
// Evaluators returns the map of evaluators by groupKind // evaluatorsList converts a map of evaluators to list
func (r *GenericRegistry) Evaluators() map[schema.GroupKind]quota.Evaluator { func evaluatorsList(input map[schema.GroupResource]quota.Evaluator) []quota.Evaluator {
return r.InternalEvaluators var result []quota.Evaluator
for _, item := range input {
result = append(result, item)
}
return result
} }
...@@ -12,8 +12,8 @@ go_library( ...@@ -12,8 +12,8 @@ go_library(
deps = [ deps = [
"//pkg/quota:go_default_library", "//pkg/quota:go_default_library",
"//pkg/quota/evaluator/core:go_default_library", "//pkg/quota/evaluator/core:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library", "//pkg/quota/generic:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
], ],
) )
......
...@@ -17,15 +17,42 @@ limitations under the License. ...@@ -17,15 +17,42 @@ limitations under the License.
package install package install
import ( import (
"k8s.io/client-go/informers" "k8s.io/apimachinery/pkg/runtime/schema"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/evaluator/core" "k8s.io/kubernetes/pkg/quota/evaluator/core"
"k8s.io/kubernetes/pkg/quota/generic"
) )
// NewRegistry returns a registry of quota evaluators. // NewQuotaConfigurationForAdmission returns a quota configuration for admission control.
// If a shared informer factory is provided, it is used by evaluators rather than performing direct queries. func NewQuotaConfigurationForAdmission() quota.Configuration {
func NewRegistry(kubeClient clientset.Interface, f informers.SharedInformerFactory) quota.Registry { evaluators := core.NewEvaluators(nil)
// TODO: when quota supports resources in other api groups, we will need to merge return generic.NewConfiguration(evaluators, DefaultIgnoredResources())
return core.NewRegistry(kubeClient, f) }
// 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
} }
...@@ -20,6 +20,7 @@ import ( ...@@ -20,6 +20,7 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/tools/cache"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
) )
...@@ -39,12 +40,12 @@ type UsageStats struct { ...@@ -39,12 +40,12 @@ type UsageStats struct {
Used api.ResourceList 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 { type Evaluator interface {
// Constraints ensures that each required resource is present on item // Constraints ensures that each required resource is present on item
Constraints(required []api.ResourceName, item runtime.Object) error Constraints(required []api.ResourceName, item runtime.Object) error
// GroupKind returns the groupKind that this object knows how to evaluate // GroupResource returns the groupResource that this object knows how to evaluate
GroupKind() schema.GroupKind GroupResource() schema.GroupResource
// Handles determines if quota could be impacted by the specified attribute. // 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. // If true, admission control must perform quota processing for the operation, otherwise it is safe to ignore quota.
Handles(operation admission.Attributes) bool Handles(operation admission.Attributes) bool
...@@ -58,25 +59,25 @@ type Evaluator interface { ...@@ -58,25 +59,25 @@ type Evaluator interface {
UsageStats(options UsageStatsOptions) (UsageStats, error) 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 Registry interface { type Configuration interface {
// Evaluators returns the set Evaluator objects registered to a groupKind // IgnoredResources are ignored by quota.
Evaluators() map[schema.GroupKind]Evaluator IgnoredResources() map[schema.GroupResource]struct{}
// Evaluators for quota evaluation.
Evaluators() []Evaluator
} }
// UnionRegistry combines multiple registries. Order matters because first registry to claim a GroupKind // Registry maintains a list of evaluators
// is the "winner" type Registry interface {
type UnionRegistry []Registry // Add to registry
Add(e Evaluator)
// Evaluators returns a mapping of evaluators by group kind. // Remove from registry
func (r UnionRegistry) Evaluators() map[schema.GroupKind]Evaluator { Remove(e Evaluator)
ret := map[schema.GroupKind]Evaluator{} // Get by group resource
Get(gr schema.GroupResource) Evaluator
for i := len(r) - 1; i >= 0; i-- { // List from registry
for k, v := range r[i].Evaluators() { List() []Evaluator
ret[k] = v
}
}
return ret
} }
// ListerForResourceFunc knows how to get a lister for a specific resource
type ListerForResourceFunc func(schema.GroupVersionResource) (cache.GenericLister, error)
...@@ -247,7 +247,7 @@ func CalculateUsage(namespaceName string, scopes []api.ResourceQuotaScope, hardL ...@@ -247,7 +247,7 @@ func CalculateUsage(namespaceName string, scopes []api.ResourceQuotaScope, hardL
// look to measure updated usage stats for // look to measure updated usage stats for
hardResources := ResourceNames(hardLimits) hardResources := ResourceNames(hardLimits)
potentialResources := []api.ResourceName{} potentialResources := []api.ResourceName{}
evaluators := registry.Evaluators() evaluators := registry.List()
for _, evaluator := range evaluators { for _, evaluator := range evaluators {
potentialResources = append(potentialResources, evaluator.MatchingResources(hardResources)...) potentialResources = append(potentialResources, evaluator.MatchingResources(hardResources)...)
} }
......
...@@ -23,6 +23,7 @@ go_library( ...@@ -23,6 +23,7 @@ go_library(
"//pkg/client/listers/core/internalversion:go_default_library", "//pkg/client/listers/core/internalversion:go_default_library",
"//pkg/kubeapiserver/admission:go_default_library", "//pkg/kubeapiserver/admission:go_default_library",
"//pkg/quota:go_default_library", "//pkg/quota:go_default_library",
"//pkg/quota/generic:go_default_library",
"//pkg/util/reflector/prometheus:go_default_library", "//pkg/util/reflector/prometheus:go_default_library",
"//pkg/util/workqueue/prometheus:go_default_library", "//pkg/util/workqueue/prometheus:go_default_library",
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library", "//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
...@@ -58,14 +59,11 @@ go_test( ...@@ -58,14 +59,11 @@ go_test(
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library", "//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library", "//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/controller: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", "//pkg/quota/install:go_default_library",
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library", "//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
"//vendor/github.com/hashicorp/golang-lru: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/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1: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/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library", "//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
......
...@@ -53,16 +53,16 @@ func Register(plugins *admission.Plugins) { ...@@ -53,16 +53,16 @@ func Register(plugins *admission.Plugins) {
// quotaAdmission implements an admission controller that can enforce quota constraints // quotaAdmission implements an admission controller that can enforce quota constraints
type quotaAdmission struct { type quotaAdmission struct {
*admission.Handler *admission.Handler
config *resourcequotaapi.Configuration config *resourcequotaapi.Configuration
stopCh <-chan struct{} stopCh <-chan struct{}
registry quota.Registry quotaConfiguration quota.Configuration
numEvaluators int numEvaluators int
quotaAccessor *quotaAccessor quotaAccessor *quotaAccessor
evaluator Evaluator evaluator Evaluator
} }
var _ = kubeapiserveradmission.WantsInternalKubeClientSet(&quotaAdmission{}) var _ = kubeapiserveradmission.WantsInternalKubeClientSet(&quotaAdmission{})
var _ = kubeapiserveradmission.WantsQuotaRegistry(&quotaAdmission{}) var _ = kubeapiserveradmission.WantsQuotaConfiguration(&quotaAdmission{})
type liveLookupEntry struct { type liveLookupEntry struct {
expiry time.Time expiry time.Time
...@@ -95,9 +95,9 @@ func (a *quotaAdmission) SetInternalKubeInformerFactory(f informers.SharedInform ...@@ -95,9 +95,9 @@ func (a *quotaAdmission) SetInternalKubeInformerFactory(f informers.SharedInform
a.quotaAccessor.lister = f.Core().InternalVersion().ResourceQuotas().Lister() a.quotaAccessor.lister = f.Core().InternalVersion().ResourceQuotas().Lister()
} }
func (a *quotaAdmission) SetQuotaRegistry(r quota.Registry) { func (a *quotaAdmission) SetQuotaConfiguration(c quota.Configuration) {
a.registry = r a.quotaConfiguration = c
a.evaluator = NewQuotaEvaluator(a.quotaAccessor, a.registry, nil, a.config, a.numEvaluators, a.stopCh) a.evaluator = NewQuotaEvaluator(a.quotaAccessor, a.quotaConfiguration, nil, a.config, a.numEvaluators, a.stopCh)
} }
// Validate ensures an authorizer is set. // Validate ensures an authorizer is set.
...@@ -111,8 +111,8 @@ func (a *quotaAdmission) Validate() error { ...@@ -111,8 +111,8 @@ func (a *quotaAdmission) Validate() error {
if a.quotaAccessor.lister == nil { if a.quotaAccessor.lister == nil {
return fmt.Errorf("missing quotaAccessor.lister") return fmt.Errorf("missing quotaAccessor.lister")
} }
if a.registry == nil { if a.quotaConfiguration == nil {
return fmt.Errorf("missing registry") return fmt.Errorf("missing quotaConfiguration")
} }
if a.evaluator == nil { if a.evaluator == nil {
return fmt.Errorf("missing evaluator") return fmt.Errorf("missing evaluator")
...@@ -126,5 +126,9 @@ func (a *quotaAdmission) Admit(attr admission.Attributes) (err error) { ...@@ -126,5 +126,9 @@ func (a *quotaAdmission) Admit(attr admission.Attributes) (err error) {
if attr.GetSubresource() != "" { if attr.GetSubresource() != "" {
return nil return nil
} }
// ignore all operations that are not namespaced
if attr.GetNamespace() == "" {
return nil
}
return a.evaluator.Evaluate(attr) return a.evaluator.Evaluate(attr)
} }
...@@ -34,6 +34,7 @@ import ( ...@@ -34,6 +34,7 @@ import (
"k8s.io/client-go/util/workqueue" "k8s.io/client-go/util/workqueue"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota" "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/reflector/prometheus" // for reflector metric registration
_ "k8s.io/kubernetes/pkg/util/workqueue/prometheus" // for workqueue metric registration _ "k8s.io/kubernetes/pkg/util/workqueue/prometheus" // for workqueue metric registration
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota" resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
...@@ -51,6 +52,9 @@ type quotaEvaluator struct { ...@@ -51,6 +52,9 @@ type quotaEvaluator struct {
// lockAcquisitionFunc acquires any required locks and returns a cleanup method to defer // lockAcquisitionFunc acquires any required locks and returns a cleanup method to defer
lockAcquisitionFunc func([]api.ResourceQuota) func() lockAcquisitionFunc func([]api.ResourceQuota) func()
// how quota was configured
quotaConfiguration quota.Configuration
// registry that knows how to measure usage for objects // registry that knows how to measure usage for objects
registry quota.Registry registry quota.Registry
...@@ -106,16 +110,18 @@ func newAdmissionWaiter(a admission.Attributes) *admissionWaiter { ...@@ -106,16 +110,18 @@ func newAdmissionWaiter(a admission.Attributes) *admissionWaiter {
// NewQuotaEvaluator configures an admission controller that can enforce quota constraints // 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 // 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 // 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 we get a nil config, just create an empty default.
if config == nil { if config == nil {
config = &resourcequotaapi.Configuration{} config = &resourcequotaapi.Configuration{}
} }
return &quotaEvaluator{ return &quotaEvaluator{
quotaAccessor: quotaAccessor, quotaAccessor: quotaAccessor,
lockAcquisitionFunc: lockAcquisitionFunc, lockAcquisitionFunc: lockAcquisitionFunc,
registry: registry, quotaConfiguration: quotaConfiguration,
registry: generic.NewRegistry(quotaConfiguration.Evaluators()),
queue: workqueue.NewNamed("admission_quota_controller"), queue: workqueue.NewNamed("admission_quota_controller"),
work: map[string][]*admissionWaiter{}, work: map[string][]*admissionWaiter{},
...@@ -365,9 +371,8 @@ func limitedByDefault(usage api.ResourceList, limitedResources []resourcequotaap ...@@ -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 // 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) { func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.Attributes) ([]api.ResourceQuota, error) {
namespace := a.GetNamespace() namespace := a.GetNamespace()
evaluators := e.registry.Evaluators() evaluator := e.registry.Get(a.GetResource().GroupResource())
evaluator, found := evaluators[a.GetKind().GroupKind()] if evaluator == nil {
if !found {
return quotas, nil return quotas, nil
} }
...@@ -516,18 +521,27 @@ func (e *quotaEvaluator) Evaluate(a admission.Attributes) error { ...@@ -516,18 +521,27 @@ func (e *quotaEvaluator) Evaluate(a admission.Attributes) error {
go e.run() go e.run()
}) })
// if we do not know how to evaluate use for this kind, just ignore // is this resource ignored?
evaluators := e.registry.Evaluators() gvr := a.GetResource()
evaluator, found := evaluators[a.GetKind().GroupKind()] gr := gvr.GroupResource()
if !found { if _, ok := e.quotaConfiguration.IgnoredResources()[gr]; ok {
return nil 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 // for this kind, check if the operation could mutate any quota resources
// if no resources tracked by quota are impacted, then just return // if no resources tracked by quota are impacted, then just return
if !evaluator.Handles(a) { if !evaluator.Handles(a) {
return nil return nil
} }
waiter := newAdmissionWaiter(a) waiter := newAdmissionWaiter(a)
e.addWork(waiter) e.addWork(waiter)
......
...@@ -38,6 +38,7 @@ go_library( ...@@ -38,6 +38,7 @@ go_library(
"//vendor/github.com/onsi/gomega:go_default_library", "//vendor/github.com/onsi/gomega:go_default_library",
"//vendor/github.com/stretchr/testify/assert: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/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/api/scheduling/v1alpha1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
......
...@@ -21,6 +21,7 @@ import ( ...@@ -21,6 +21,7 @@ import (
"time" "time"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
...@@ -409,6 +410,41 @@ var _ = SIGDescribe("ResourceQuota", func() { ...@@ -409,6 +410,41 @@ var _ = SIGDescribe("ResourceQuota", func() {
Expect(err).NotTo(HaveOccurred()) 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() { It("should create a ResourceQuota and capture the life of a persistent volume claim. [sig-storage]", func() {
By("Creating a ResourceQuota") By("Creating a ResourceQuota")
quotaName := "test-quota" quotaName := "test-quota"
...@@ -708,6 +744,8 @@ func newTestResourceQuota(name string) *v1.ResourceQuota { ...@@ -708,6 +744,8 @@ func newTestResourceQuota(name string) *v1.ResourceQuota {
hard[v1.ResourceRequestsStorage] = resource.MustParse("10Gi") hard[v1.ResourceRequestsStorage] = resource.MustParse("10Gi")
hard[core.V1ResourceByStorageClass(classGold, v1.ResourcePersistentVolumeClaims)] = resource.MustParse("10") hard[core.V1ResourceByStorageClass(classGold, v1.ResourcePersistentVolumeClaims)] = resource.MustParse("10")
hard[core.V1ResourceByStorageClass(classGold, v1.ResourceRequestsStorage)] = resource.MustParse("10Gi") 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{ return &v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: name}, ObjectMeta: metav1.ObjectMeta{Name: name},
Spec: v1.ResourceQuotaSpec{Hard: hard}, Spec: v1.ResourceQuotaSpec{Hard: hard},
...@@ -784,6 +822,33 @@ func newTestReplicationControllerForQuota(name, image string, replicas int32) *v ...@@ -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 // newTestServiceForQuota returns a simple service
func newTestServiceForQuota(name string, serviceType v1.ServiceType) *v1.Service { func newTestServiceForQuota(name string, serviceType v1.ServiceType) *v1.Service {
return &v1.Service{ return &v1.Service{
......
...@@ -15,7 +15,6 @@ go_test( ...@@ -15,7 +15,6 @@ go_test(
importpath = "k8s.io/kubernetes/test/integration/quota", importpath = "k8s.io/kubernetes/test/integration/quota",
tags = ["integration"], tags = ["integration"],
deps = [ deps = [
"//pkg/api:go_default_library",
"//pkg/api/testapi:go_default_library", "//pkg/api/testapi:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library", "//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library", "//pkg/client/informers/informers_generated/internalversion:go_default_library",
...@@ -23,6 +22,7 @@ go_test( ...@@ -23,6 +22,7 @@ go_test(
"//pkg/controller/replication:go_default_library", "//pkg/controller/replication:go_default_library",
"//pkg/controller/resourcequota:go_default_library", "//pkg/controller/resourcequota:go_default_library",
"//pkg/kubeapiserver/admission:go_default_library", "//pkg/kubeapiserver/admission:go_default_library",
"//pkg/quota/generic:go_default_library",
"//pkg/quota/install:go_default_library", "//pkg/quota/install:go_default_library",
"//plugin/pkg/admission/resourcequota:go_default_library", "//plugin/pkg/admission/resourcequota:go_default_library",
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library", "//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
...@@ -32,7 +32,6 @@ go_test( ...@@ -32,7 +32,6 @@ go_test(
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//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/fields:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels: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/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library", "//vendor/k8s.io/client-go/informers:go_default_library",
......
...@@ -28,14 +28,12 @@ import ( ...@@ -28,14 +28,12 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/record" "k8s.io/client-go/tools/record"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
internalinformers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion" internalinformers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
...@@ -43,6 +41,7 @@ import ( ...@@ -43,6 +41,7 @@ import (
replicationcontroller "k8s.io/kubernetes/pkg/controller/replication" replicationcontroller "k8s.io/kubernetes/pkg/controller/replication"
resourcequotacontroller "k8s.io/kubernetes/pkg/controller/resourcequota" resourcequotacontroller "k8s.io/kubernetes/pkg/controller/resourcequota"
kubeadmission "k8s.io/kubernetes/pkg/kubeapiserver/admission" kubeadmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
"k8s.io/kubernetes/pkg/quota/generic"
quotainstall "k8s.io/kubernetes/pkg/quota/install" quotainstall "k8s.io/kubernetes/pkg/quota/install"
"k8s.io/kubernetes/plugin/pkg/admission/resourcequota" "k8s.io/kubernetes/plugin/pkg/admission/resourcequota"
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota" resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
...@@ -74,8 +73,8 @@ func TestQuota(t *testing.T) { ...@@ -74,8 +73,8 @@ func TestQuota(t *testing.T) {
admission.(kubeadmission.WantsInternalKubeClientSet).SetInternalKubeClientSet(internalClientset) admission.(kubeadmission.WantsInternalKubeClientSet).SetInternalKubeClientSet(internalClientset)
internalInformers := internalinformers.NewSharedInformerFactory(internalClientset, controller.NoResyncPeriodFunc()) internalInformers := internalinformers.NewSharedInformerFactory(internalClientset, controller.NoResyncPeriodFunc())
admission.(kubeadmission.WantsInternalKubeInformerFactory).SetInternalKubeInformerFactory(internalInformers) admission.(kubeadmission.WantsInternalKubeInformerFactory).SetInternalKubeInformerFactory(internalInformers)
quotaRegistry := quotainstall.NewRegistry(nil, nil) qca := quotainstall.NewQuotaConfigurationForAdmission()
admission.(kubeadmission.WantsQuotaRegistry).SetQuotaRegistry(quotaRegistry) admission.(kubeadmission.WantsQuotaConfiguration).SetQuotaConfiguration(qca)
defer close(admissionCh) defer close(admissionCh)
masterConfig := framework.NewIntegrationTestMasterConfig() masterConfig := framework.NewIntegrationTestMasterConfig()
...@@ -101,22 +100,33 @@ func TestQuota(t *testing.T) { ...@@ -101,22 +100,33 @@ func TestQuota(t *testing.T) {
rm.SetEventRecorder(&record.FakeRecorder{}) rm.SetEventRecorder(&record.FakeRecorder{})
go rm.Run(3, controllerCh) go rm.Run(3, controllerCh)
resourceQuotaRegistry := quotainstall.NewRegistry(clientset, nil) discoveryFunc := clientset.Discovery().ServerPreferredNamespacedResources
groupKindsToReplenish := []schema.GroupKind{ listerFuncForResource := generic.ListerFuncForResourceFunc(informers.ForResource)
api.Kind("Pod"), qc := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource)
} informersStarted := make(chan struct{})
resourceQuotaControllerOptions := &resourcequotacontroller.ResourceQuotaControllerOptions{ resourceQuotaControllerOptions := &resourcequotacontroller.ResourceQuotaControllerOptions{
QuotaClient: clientset.Core(), QuotaClient: clientset.Core(),
ResourceQuotaInformer: informers.Core().V1().ResourceQuotas(), ResourceQuotaInformer: informers.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.NoResyncPeriodFunc, ResyncPeriod: controller.NoResyncPeriodFunc,
Registry: resourceQuotaRegistry, InformerFactory: informers,
GroupKindsToReplenish: groupKindsToReplenish,
ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc, ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
ControllerFactory: resourcequotacontroller.NewReplenishmentControllerFactory(informers), DiscoveryFunc: discoveryFunc,
IgnoredResourcesFunc: qc.IgnoredResources,
InformersStarted: informersStarted,
Registry: generic.NewRegistry(qc.Evaluators()),
}
resourceQuotaController, err := resourcequotacontroller.NewResourceQuotaController(resourceQuotaControllerOptions)
if err != nil {
t.Fatalf("unexpected err: %v", err)
} }
go resourcequotacontroller.NewResourceQuotaController(resourceQuotaControllerOptions).Run(2, controllerCh) 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) internalInformers.Start(controllerCh)
informers.Start(controllerCh) informers.Start(controllerCh)
close(informersStarted)
startTime := time.Now() startTime := time.Now()
scale(t, ns2.Name, clientset) scale(t, ns2.Name, clientset)
...@@ -158,7 +168,6 @@ func waitForQuota(t *testing.T, quota *v1.ResourceQuota, clientset *clientset.Cl ...@@ -158,7 +168,6 @@ func waitForQuota(t *testing.T, quota *v1.ResourceQuota, clientset *clientset.Cl
default: default:
return false, nil return false, nil
} }
switch cast := event.Object.(type) { switch cast := event.Object.(type) {
case *v1.ResourceQuota: case *v1.ResourceQuota:
if len(cast.Status.Hard) > 0 { if len(cast.Status.Hard) > 0 {
...@@ -254,7 +263,7 @@ func TestQuotaLimitedResourceDenial(t *testing.T) { ...@@ -254,7 +263,7 @@ func TestQuotaLimitedResourceDenial(t *testing.T) {
}, },
}, },
} }
quotaRegistry := quotainstall.NewRegistry(nil, nil) qca := quotainstall.NewQuotaConfigurationForAdmission()
admission, err := resourcequota.NewResourceQuota(config, 5, admissionCh) admission, err := resourcequota.NewResourceQuota(config, 5, admissionCh)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
...@@ -262,7 +271,7 @@ func TestQuotaLimitedResourceDenial(t *testing.T) { ...@@ -262,7 +271,7 @@ func TestQuotaLimitedResourceDenial(t *testing.T) {
admission.(kubeadmission.WantsInternalKubeClientSet).SetInternalKubeClientSet(internalClientset) admission.(kubeadmission.WantsInternalKubeClientSet).SetInternalKubeClientSet(internalClientset)
internalInformers := internalinformers.NewSharedInformerFactory(internalClientset, controller.NoResyncPeriodFunc()) internalInformers := internalinformers.NewSharedInformerFactory(internalClientset, controller.NoResyncPeriodFunc())
admission.(kubeadmission.WantsInternalKubeInformerFactory).SetInternalKubeInformerFactory(internalInformers) admission.(kubeadmission.WantsInternalKubeInformerFactory).SetInternalKubeInformerFactory(internalInformers)
admission.(kubeadmission.WantsQuotaRegistry).SetQuotaRegistry(quotaRegistry) admission.(kubeadmission.WantsQuotaConfiguration).SetQuotaConfiguration(qca)
defer close(admissionCh) defer close(admissionCh)
masterConfig := framework.NewIntegrationTestMasterConfig() masterConfig := framework.NewIntegrationTestMasterConfig()
...@@ -286,22 +295,33 @@ func TestQuotaLimitedResourceDenial(t *testing.T) { ...@@ -286,22 +295,33 @@ func TestQuotaLimitedResourceDenial(t *testing.T) {
rm.SetEventRecorder(&record.FakeRecorder{}) rm.SetEventRecorder(&record.FakeRecorder{})
go rm.Run(3, controllerCh) go rm.Run(3, controllerCh)
resourceQuotaRegistry := quotainstall.NewRegistry(clientset, nil) discoveryFunc := clientset.Discovery().ServerPreferredNamespacedResources
groupKindsToReplenish := []schema.GroupKind{ listerFuncForResource := generic.ListerFuncForResourceFunc(informers.ForResource)
api.Kind("Pod"), qc := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource)
} informersStarted := make(chan struct{})
resourceQuotaControllerOptions := &resourcequotacontroller.ResourceQuotaControllerOptions{ resourceQuotaControllerOptions := &resourcequotacontroller.ResourceQuotaControllerOptions{
QuotaClient: clientset.Core(), QuotaClient: clientset.Core(),
ResourceQuotaInformer: informers.Core().V1().ResourceQuotas(), ResourceQuotaInformer: informers.Core().V1().ResourceQuotas(),
ResyncPeriod: controller.NoResyncPeriodFunc, ResyncPeriod: controller.NoResyncPeriodFunc,
Registry: resourceQuotaRegistry, InformerFactory: informers,
GroupKindsToReplenish: groupKindsToReplenish,
ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc, ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
ControllerFactory: resourcequotacontroller.NewReplenishmentControllerFactory(informers), DiscoveryFunc: discoveryFunc,
IgnoredResourcesFunc: qc.IgnoredResources,
InformersStarted: informersStarted,
Registry: generic.NewRegistry(qc.Evaluators()),
}
resourceQuotaController, err := resourcequotacontroller.NewResourceQuotaController(resourceQuotaControllerOptions)
if err != nil {
t.Fatalf("unexpected err: %v", err)
} }
go resourcequotacontroller.NewResourceQuotaController(resourceQuotaControllerOptions).Run(2, controllerCh) 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) internalInformers.Start(controllerCh)
informers.Start(controllerCh) informers.Start(controllerCh)
close(informersStarted)
// try to create a pod // try to create a pod
pod := &v1.Pod{ pod := &v1.Pod{
...@@ -323,6 +343,7 @@ func TestQuotaLimitedResourceDenial(t *testing.T) { ...@@ -323,6 +343,7 @@ func TestQuotaLimitedResourceDenial(t *testing.T) {
} }
// now create a covering quota // now create a covering quota
// note: limited resource does a matchContains, so we now have "pods" matching "pods" and "count/pods"
quota := &v1.ResourceQuota{ quota := &v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "quota", Name: "quota",
...@@ -330,7 +351,8 @@ func TestQuotaLimitedResourceDenial(t *testing.T) { ...@@ -330,7 +351,8 @@ func TestQuotaLimitedResourceDenial(t *testing.T) {
}, },
Spec: v1.ResourceQuotaSpec{ Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{ Hard: v1.ResourceList{
v1.ResourcePods: resource.MustParse("1000"), v1.ResourcePods: resource.MustParse("1000"),
v1.ResourceName("count/pods"): resource.MustParse("1000"),
}, },
}, },
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment