123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322 |
- /*
- Copyright 2014 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 (
- "context"
- "fmt"
- "strconv"
- "strings"
- "testing"
- "time"
- lru "github.com/hashicorp/golang-lru"
- corev1 "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/resource"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/util/sets"
- "k8s.io/apiserver/pkg/admission"
- utilfeature "k8s.io/apiserver/pkg/util/feature"
- "k8s.io/client-go/informers"
- "k8s.io/client-go/kubernetes/fake"
- testcore "k8s.io/client-go/testing"
- "k8s.io/client-go/tools/cache"
- featuregatetesting "k8s.io/component-base/featuregate/testing"
- api "k8s.io/kubernetes/pkg/apis/core"
- "k8s.io/kubernetes/pkg/controller"
- "k8s.io/kubernetes/pkg/features"
- "k8s.io/kubernetes/pkg/quota/v1/generic"
- "k8s.io/kubernetes/pkg/quota/v1/install"
- resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
- )
- func getResourceList(cpu, memory string) api.ResourceList {
- res := api.ResourceList{}
- if cpu != "" {
- res[api.ResourceCPU] = resource.MustParse(cpu)
- }
- if memory != "" {
- res[api.ResourceMemory] = resource.MustParse(memory)
- }
- return res
- }
- func getResourceRequirements(requests, limits api.ResourceList) api.ResourceRequirements {
- res := api.ResourceRequirements{}
- res.Requests = requests
- res.Limits = limits
- return res
- }
- func validPod(name string, numContainers int, resources api.ResourceRequirements) *api.Pod {
- pod := &api.Pod{
- ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"},
- Spec: api.PodSpec{},
- }
- pod.Spec.Containers = make([]api.Container, 0, numContainers)
- for i := 0; i < numContainers; i++ {
- pod.Spec.Containers = append(pod.Spec.Containers, api.Container{
- Image: "foo:V" + strconv.Itoa(i),
- Resources: resources,
- })
- }
- return pod
- }
- func validPodWithPriority(name string, numContainers int, resources api.ResourceRequirements, priorityClass string) *api.Pod {
- pod := validPod(name, numContainers, resources)
- if priorityClass != "" {
- pod.Spec.PriorityClassName = priorityClass
- }
- return pod
- }
- func validPersistentVolumeClaim(name string, resources api.ResourceRequirements) *api.PersistentVolumeClaim {
- return &api.PersistentVolumeClaim{
- ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"},
- Spec: api.PersistentVolumeClaimSpec{
- Resources: resources,
- },
- }
- }
- func TestPrettyPrint(t *testing.T) {
- toResourceList := func(resources map[corev1.ResourceName]string) corev1.ResourceList {
- resourceList := corev1.ResourceList{}
- for key, value := range resources {
- resourceList[key] = resource.MustParse(value)
- }
- return resourceList
- }
- testCases := []struct {
- input corev1.ResourceList
- expected string
- }{
- {
- input: toResourceList(map[corev1.ResourceName]string{
- corev1.ResourceCPU: "100m",
- }),
- expected: "cpu=100m",
- },
- {
- input: toResourceList(map[corev1.ResourceName]string{
- corev1.ResourcePods: "10",
- corev1.ResourceServices: "10",
- corev1.ResourceReplicationControllers: "10",
- corev1.ResourceServicesNodePorts: "10",
- corev1.ResourceRequestsCPU: "100m",
- corev1.ResourceRequestsMemory: "100Mi",
- corev1.ResourceLimitsCPU: "100m",
- corev1.ResourceLimitsMemory: "100Mi",
- }),
- expected: "limits.cpu=100m,limits.memory=100Mi,pods=10,replicationcontrollers=10,requests.cpu=100m,requests.memory=100Mi,services=10,services.nodeports=10",
- },
- }
- for i, testCase := range testCases {
- result := prettyPrint(testCase.input)
- if result != testCase.expected {
- t.Errorf("Pretty print did not give stable sorted output[%d], expected %v, but got %v", i, testCase.expected, result)
- }
- }
- }
- // TestAdmissionIgnoresDelete verifies that the admission controller ignores delete operations
- func TestAdmissionIgnoresDelete(t *testing.T) {
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset()
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- namespace := "default"
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(nil, nil, api.Kind("Pod").WithVersion("version"), namespace, "name", corev1.Resource("pods").WithVersion("version"), "", admission.Delete, &metav1.DeleteOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("ResourceQuota should admit all deletes: %v", err)
- }
- }
- // TestAdmissionIgnoresSubresources verifies that the admission controller ignores subresources
- // It verifies that creation of a pod that would have exceeded quota is properly failed
- // It verifies that create operations to a subresource that would have exceeded quota would succeed
- func TestAdmissionIgnoresSubresources(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{}
- resourceQuota.Name = "quota"
- resourceQuota.Namespace = "test"
- resourceQuota.Status = corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{},
- Used: corev1.ResourceList{},
- }
- resourceQuota.Status.Hard[corev1.ResourceMemory] = resource.MustParse("2Gi")
- resourceQuota.Status.Used[corev1.ResourceMemory] = resource.MustParse("1Gi")
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset()
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- newPod := validPod("123", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err == nil {
- t.Errorf("Expected an error because the pod exceeded allowed quota")
- }
- err = handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "subresource", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Did not expect an error because the action went to a subresource: %v", err)
- }
- }
- // TestAdmitBelowQuotaLimit verifies that a pod when created has its usage reflected on the quota
- func TestAdmitBelowQuotaLimit(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("3"),
- corev1.ResourceMemory: resource.MustParse("100Gi"),
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("1"),
- corev1.ResourceMemory: resource.MustParse("50Gi"),
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- }
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- if len(kubeClient.Actions()) == 0 {
- t.Errorf("Expected a client action")
- }
- expectedActionSet := sets.NewString(
- strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
- )
- actionSet := sets.NewString()
- for _, action := range kubeClient.Actions() {
- actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
- }
- if !actionSet.HasAll(expectedActionSet.List()...) {
- t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
- }
- decimatedActions := removeListWatch(kubeClient.Actions())
- lastActionIndex := len(decimatedActions) - 1
- usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
- expectedUsage := corev1.ResourceQuota{
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("3"),
- corev1.ResourceMemory: resource.MustParse("100Gi"),
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("1100m"),
- corev1.ResourceMemory: resource.MustParse("52Gi"),
- corev1.ResourcePods: resource.MustParse("4"),
- },
- },
- }
- for k, v := range expectedUsage.Status.Used {
- actual := usage.Status.Used[k]
- actualValue := actual.String()
- expectedValue := v.String()
- if expectedValue != actualValue {
- t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
- }
- }
- }
- // TestAdmitDryRun verifies that a pod when created with dry-run doesn not have its usage reflected on the quota
- // and that dry-run requests can still be rejected if they would exceed the quota
- func TestAdmitDryRun(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("3"),
- corev1.ResourceMemory: resource.MustParse("100Gi"),
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("1"),
- corev1.ResourceMemory: resource.MustParse("50Gi"),
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- }
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, true, nil), nil)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- newPod = validPod("too-large-pod", 1, getResourceRequirements(getResourceList("100m", "60Gi"), getResourceList("", "")))
- err = handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, true, nil), nil)
- if err == nil {
- t.Errorf("Expected error but got none")
- }
- if len(kubeClient.Actions()) != 0 {
- t.Errorf("Expected no client action on dry-run")
- }
- }
- // TestAdmitHandlesOldObjects verifies that admit handles updates correctly with old objects
- func TestAdmitHandlesOldObjects(t *testing.T) {
- // in this scenario, the old quota was based on a service type=loadbalancer
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceServices: resource.MustParse("10"),
- corev1.ResourceServicesLoadBalancers: resource.MustParse("10"),
- corev1.ResourceServicesNodePorts: resource.MustParse("10"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceServices: resource.MustParse("1"),
- corev1.ResourceServicesLoadBalancers: resource.MustParse("1"),
- corev1.ResourceServicesNodePorts: resource.MustParse("0"),
- },
- },
- }
- // start up quota system
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- // old service was a load balancer, but updated version is a node port.
- existingService := &api.Service{
- ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test", ResourceVersion: "1"},
- Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
- }
- newService := &api.Service{
- ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test"},
- Spec: api.ServiceSpec{
- Type: api.ServiceTypeNodePort,
- Ports: []api.ServicePort{{Port: 1234}},
- },
- }
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newService, existingService, api.Kind("Service").WithVersion("version"), newService.Namespace, newService.Name, corev1.Resource("services").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- if len(kubeClient.Actions()) == 0 {
- t.Errorf("Expected a client action")
- }
- // the only action should have been to update the quota (since we should not have fetched the previous item)
- expectedActionSet := sets.NewString(
- strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
- )
- actionSet := sets.NewString()
- for _, action := range kubeClient.Actions() {
- actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
- }
- if !actionSet.HasAll(expectedActionSet.List()...) {
- t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
- }
- // verify usage decremented the loadbalancer, and incremented the nodeport, but kept the service the same.
- decimatedActions := removeListWatch(kubeClient.Actions())
- lastActionIndex := len(decimatedActions) - 1
- usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
- // Verify service usage. Since we don't add negative values, the corev1.ResourceServicesLoadBalancers
- // will remain on last reported value
- expectedUsage := corev1.ResourceQuota{
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceServices: resource.MustParse("10"),
- corev1.ResourceServicesLoadBalancers: resource.MustParse("10"),
- corev1.ResourceServicesNodePorts: resource.MustParse("10"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceServices: resource.MustParse("1"),
- corev1.ResourceServicesLoadBalancers: resource.MustParse("1"),
- corev1.ResourceServicesNodePorts: resource.MustParse("1"),
- },
- },
- }
- for k, v := range expectedUsage.Status.Used {
- actual := usage.Status.Used[k]
- actualValue := actual.String()
- expectedValue := v.String()
- if expectedValue != actualValue {
- t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
- }
- }
- }
- func TestAdmitHandlesNegativePVCUpdates(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePersistentVolumeClaims: resource.MustParse("3"),
- corev1.ResourceRequestsStorage: resource.MustParse("100Gi"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePersistentVolumeClaims: resource.MustParse("1"),
- corev1.ResourceRequestsStorage: resource.MustParse("10Gi"),
- },
- },
- }
- // start up quota system
- stopCh := make(chan struct{})
- defer close(stopCh)
- defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExpandPersistentVolumes, true)()
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- oldPVC := &api.PersistentVolumeClaim{
- ObjectMeta: metav1.ObjectMeta{Name: "pvc-to-update", Namespace: "test", ResourceVersion: "1"},
- Spec: api.PersistentVolumeClaimSpec{
- Resources: getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("10Gi")}, api.ResourceList{}),
- },
- }
- newPVC := &api.PersistentVolumeClaim{
- ObjectMeta: metav1.ObjectMeta{Name: "pvc-to-update", Namespace: "test"},
- Spec: api.PersistentVolumeClaimSpec{
- Resources: getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("5Gi")}, api.ResourceList{}),
- },
- }
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPVC, oldPVC, api.Kind("PersistentVolumeClaim").WithVersion("version"), newPVC.Namespace, newPVC.Name, corev1.Resource("persistentvolumeclaims").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- if len(kubeClient.Actions()) != 0 {
- t.Errorf("No client action should be taken in case of negative updates")
- }
- }
- func TestAdmitHandlesPVCUpdates(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePersistentVolumeClaims: resource.MustParse("3"),
- corev1.ResourceRequestsStorage: resource.MustParse("100Gi"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePersistentVolumeClaims: resource.MustParse("1"),
- corev1.ResourceRequestsStorage: resource.MustParse("10Gi"),
- },
- },
- }
- defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExpandPersistentVolumes, true)()
- // start up quota system
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- oldPVC := &api.PersistentVolumeClaim{
- ObjectMeta: metav1.ObjectMeta{Name: "pvc-to-update", Namespace: "test", ResourceVersion: "1"},
- Spec: api.PersistentVolumeClaimSpec{
- Resources: getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("10Gi")}, api.ResourceList{}),
- },
- }
- newPVC := &api.PersistentVolumeClaim{
- ObjectMeta: metav1.ObjectMeta{Name: "pvc-to-update", Namespace: "test"},
- Spec: api.PersistentVolumeClaimSpec{
- Resources: getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("15Gi")}, api.ResourceList{}),
- },
- }
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPVC, oldPVC, api.Kind("PersistentVolumeClaim").WithVersion("version"), newPVC.Namespace, newPVC.Name, corev1.Resource("persistentvolumeclaims").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- if len(kubeClient.Actions()) == 0 {
- t.Errorf("Expected a client action")
- }
- // the only action should have been to update the quota (since we should not have fetched the previous item)
- expectedActionSet := sets.NewString(
- strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
- )
- actionSet := sets.NewString()
- for _, action := range kubeClient.Actions() {
- actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
- }
- if !actionSet.HasAll(expectedActionSet.List()...) {
- t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
- }
- decimatedActions := removeListWatch(kubeClient.Actions())
- lastActionIndex := len(decimatedActions) - 1
- usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
- expectedUsage := corev1.ResourceQuota{
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePersistentVolumeClaims: resource.MustParse("3"),
- corev1.ResourceRequestsStorage: resource.MustParse("100Gi"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePersistentVolumeClaims: resource.MustParse("1"),
- corev1.ResourceRequestsStorage: resource.MustParse("15Gi"),
- },
- },
- }
- for k, v := range expectedUsage.Status.Used {
- actual := usage.Status.Used[k]
- actualValue := actual.String()
- expectedValue := v.String()
- if expectedValue != actualValue {
- t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
- }
- }
- }
- // TestAdmitHandlesCreatingUpdates verifies that admit handles updates which behave as creates
- func TestAdmitHandlesCreatingUpdates(t *testing.T) {
- // in this scenario, there is an existing service
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceServices: resource.MustParse("10"),
- corev1.ResourceServicesLoadBalancers: resource.MustParse("10"),
- corev1.ResourceServicesNodePorts: resource.MustParse("10"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceServices: resource.MustParse("1"),
- corev1.ResourceServicesLoadBalancers: resource.MustParse("1"),
- corev1.ResourceServicesNodePorts: resource.MustParse("0"),
- },
- },
- }
- // start up quota system
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- // old service didn't exist, so this update is actually a create
- oldService := &api.Service{
- ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test", ResourceVersion: ""},
- Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
- }
- newService := &api.Service{
- ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test"},
- Spec: api.ServiceSpec{
- Type: api.ServiceTypeNodePort,
- Ports: []api.ServicePort{{Port: 1234}},
- },
- }
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newService, oldService, api.Kind("Service").WithVersion("version"), newService.Namespace, newService.Name, corev1.Resource("services").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- if len(kubeClient.Actions()) == 0 {
- t.Errorf("Expected a client action")
- }
- // the only action should have been to update the quota (since we should not have fetched the previous item)
- expectedActionSet := sets.NewString(
- strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
- )
- actionSet := sets.NewString()
- for _, action := range kubeClient.Actions() {
- actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
- }
- if !actionSet.HasAll(expectedActionSet.List()...) {
- t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
- }
- // verify that the "old" object was ignored for calculating the new usage
- decimatedActions := removeListWatch(kubeClient.Actions())
- lastActionIndex := len(decimatedActions) - 1
- usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
- expectedUsage := corev1.ResourceQuota{
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceServices: resource.MustParse("10"),
- corev1.ResourceServicesLoadBalancers: resource.MustParse("10"),
- corev1.ResourceServicesNodePorts: resource.MustParse("10"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceServices: resource.MustParse("2"),
- corev1.ResourceServicesLoadBalancers: resource.MustParse("1"),
- corev1.ResourceServicesNodePorts: resource.MustParse("1"),
- },
- },
- }
- for k, v := range expectedUsage.Status.Used {
- actual := usage.Status.Used[k]
- actualValue := actual.String()
- expectedValue := v.String()
- if expectedValue != actualValue {
- t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
- }
- }
- }
- // TestAdmitExceedQuotaLimit verifies that if a pod exceeded allowed usage that its rejected during admission.
- func TestAdmitExceedQuotaLimit(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("3"),
- corev1.ResourceMemory: resource.MustParse("100Gi"),
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("1"),
- corev1.ResourceMemory: resource.MustParse("50Gi"),
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- }
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err == nil {
- t.Errorf("Expected an error exceeding quota")
- }
- }
- // TestAdmitEnforceQuotaConstraints verifies that if a quota tracks a particular resource that that resource is
- // specified on the pod. In this case, we create a quota that tracks cpu request, memory request, and memory limit.
- // We ensure that a pod that does not specify a memory limit that it fails in admission.
- func TestAdmitEnforceQuotaConstraints(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("3"),
- corev1.ResourceMemory: resource.MustParse("100Gi"),
- corev1.ResourceLimitsMemory: resource.MustParse("200Gi"),
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("1"),
- corev1.ResourceMemory: resource.MustParse("50Gi"),
- corev1.ResourceLimitsMemory: resource.MustParse("100Gi"),
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- }
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- // verify all values are specified as required on the quota
- newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("200m", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err == nil {
- t.Errorf("Expected an error because the pod does not specify a memory limit")
- }
- }
- // TestAdmitPodInNamespaceWithoutQuota ensures that if a namespace has no quota, that a pod can get in
- func TestAdmitPodInNamespaceWithoutQuota(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "other", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("3"),
- corev1.ResourceMemory: resource.MustParse("100Gi"),
- corev1.ResourceLimitsMemory: resource.MustParse("200Gi"),
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("1"),
- corev1.ResourceMemory: resource.MustParse("50Gi"),
- corev1.ResourceLimitsMemory: resource.MustParse("100Gi"),
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- }
- liveLookupCache, err := lru.New(100)
- if err != nil {
- t.Fatal(err)
- }
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- quotaAccessor.liveLookupCache = liveLookupCache
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- // Add to the index
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("200m", "")))
- // Add to the lru cache so we do not do a live client lookup
- liveLookupCache.Add(newPod.Namespace, liveLookupEntry{expiry: time.Now().Add(time.Duration(30 * time.Second)), items: []*corev1.ResourceQuota{}})
- err = handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Did not expect an error because the pod is in a different namespace than the quota")
- }
- }
- // TestAdmitBelowTerminatingQuotaLimit ensures that terminating pods are charged to the right quota.
- // It creates a terminating and non-terminating quota, and creates a terminating pod.
- // It ensures that the terminating quota is incremented, and the non-terminating quota is not.
- func TestAdmitBelowTerminatingQuotaLimit(t *testing.T) {
- resourceQuotaNonTerminating := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota-non-terminating", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeNotTerminating},
- },
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("3"),
- corev1.ResourceMemory: resource.MustParse("100Gi"),
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("1"),
- corev1.ResourceMemory: resource.MustParse("50Gi"),
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- }
- resourceQuotaTerminating := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota-terminating", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeTerminating},
- },
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("3"),
- corev1.ResourceMemory: resource.MustParse("100Gi"),
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("1"),
- corev1.ResourceMemory: resource.MustParse("50Gi"),
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- }
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuotaTerminating, resourceQuotaNonTerminating)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuotaNonTerminating)
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuotaTerminating)
- // create a pod that has an active deadline
- newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("", "")))
- activeDeadlineSeconds := int64(30)
- newPod.Spec.ActiveDeadlineSeconds = &activeDeadlineSeconds
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- if len(kubeClient.Actions()) == 0 {
- t.Errorf("Expected a client action")
- }
- expectedActionSet := sets.NewString(
- strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
- )
- actionSet := sets.NewString()
- for _, action := range kubeClient.Actions() {
- actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
- }
- if !actionSet.HasAll(expectedActionSet.List()...) {
- t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
- }
- decimatedActions := removeListWatch(kubeClient.Actions())
- lastActionIndex := len(decimatedActions) - 1
- usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
- // ensure only the quota-terminating was updated
- if usage.Name != resourceQuotaTerminating.Name {
- t.Errorf("Incremented the wrong quota, expected %v, actual %v", resourceQuotaTerminating.Name, usage.Name)
- }
- expectedUsage := corev1.ResourceQuota{
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("3"),
- corev1.ResourceMemory: resource.MustParse("100Gi"),
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("1100m"),
- corev1.ResourceMemory: resource.MustParse("52Gi"),
- corev1.ResourcePods: resource.MustParse("4"),
- },
- },
- }
- for k, v := range expectedUsage.Status.Used {
- actual := usage.Status.Used[k]
- actualValue := actual.String()
- expectedValue := v.String()
- if expectedValue != actualValue {
- t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
- }
- }
- }
- // TestAdmitBelowBestEffortQuotaLimit creates a best effort and non-best effort quota.
- // It verifies that best effort pods are properly scoped to the best effort quota document.
- func TestAdmitBelowBestEffortQuotaLimit(t *testing.T) {
- resourceQuotaBestEffort := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeBestEffort},
- },
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- }
- resourceQuotaNotBestEffort := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota-not-besteffort", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeNotBestEffort},
- },
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- }
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuotaBestEffort, resourceQuotaNotBestEffort)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuotaBestEffort)
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuotaNotBestEffort)
- // create a pod that is best effort because it does not make a request for anything
- newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- expectedActionSet := sets.NewString(
- strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
- )
- actionSet := sets.NewString()
- for _, action := range kubeClient.Actions() {
- actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
- }
- if !actionSet.HasAll(expectedActionSet.List()...) {
- t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
- }
- decimatedActions := removeListWatch(kubeClient.Actions())
- lastActionIndex := len(decimatedActions) - 1
- usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
- if usage.Name != resourceQuotaBestEffort.Name {
- t.Errorf("Incremented the wrong quota, expected %v, actual %v", resourceQuotaBestEffort.Name, usage.Name)
- }
- expectedUsage := corev1.ResourceQuota{
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("4"),
- },
- },
- }
- for k, v := range expectedUsage.Status.Used {
- actual := usage.Status.Used[k]
- actualValue := actual.String()
- expectedValue := v.String()
- if expectedValue != actualValue {
- t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
- }
- }
- }
- func removeListWatch(in []testcore.Action) []testcore.Action {
- decimatedActions := []testcore.Action{}
- // list and watch resource quota is done to maintain our cache, so that's expected. Remove them from results
- for i := range in {
- if in[i].Matches("list", "resourcequotas") || in[i].Matches("watch", "resourcequotas") {
- continue
- }
- decimatedActions = append(decimatedActions, in[i])
- }
- return decimatedActions
- }
- // TestAdmitBestEffortQuotaLimitIgnoresBurstable validates that a besteffort quota does not match a resource
- // guaranteed pod.
- func TestAdmitBestEffortQuotaLimitIgnoresBurstable(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeBestEffort},
- },
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- }
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- decimatedActions := removeListWatch(kubeClient.Actions())
- if len(decimatedActions) != 0 {
- t.Errorf("Expected no client actions because the incoming pod did not match best effort quota: %v", kubeClient.Actions())
- }
- }
- func TestHasUsageStats(t *testing.T) {
- testCases := map[string]struct {
- a corev1.ResourceQuota
- relevant []corev1.ResourceName
- expected bool
- }{
- "empty": {
- a: corev1.ResourceQuota{Status: corev1.ResourceQuotaStatus{Hard: corev1.ResourceList{}}},
- relevant: []corev1.ResourceName{corev1.ResourceMemory},
- expected: true,
- },
- "hard-only": {
- a: corev1.ResourceQuota{
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceMemory: resource.MustParse("1Gi"),
- },
- Used: corev1.ResourceList{},
- },
- },
- relevant: []corev1.ResourceName{corev1.ResourceMemory},
- expected: false,
- },
- "hard-used": {
- a: corev1.ResourceQuota{
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceMemory: resource.MustParse("1Gi"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceMemory: resource.MustParse("500Mi"),
- },
- },
- },
- relevant: []corev1.ResourceName{corev1.ResourceMemory},
- expected: true,
- },
- "hard-used-relevant": {
- a: corev1.ResourceQuota{
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceMemory: resource.MustParse("1Gi"),
- corev1.ResourcePods: resource.MustParse("1"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceMemory: resource.MustParse("500Mi"),
- },
- },
- },
- relevant: []corev1.ResourceName{corev1.ResourceMemory},
- expected: true,
- },
- }
- for testName, testCase := range testCases {
- if result := hasUsageStats(&testCase.a, testCase.relevant); result != testCase.expected {
- t.Errorf("%s expected: %v, actual: %v", testName, testCase.expected, result)
- }
- }
- }
- // TestAdmissionSetsMissingNamespace verifies that if an object lacks a
- // namespace, it will be set.
- func TestAdmissionSetsMissingNamespace(t *testing.T) {
- namespace := "test"
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: namespace, ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("3"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("1"),
- },
- },
- }
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- newPod := validPod("pod-without-namespace", 1, getResourceRequirements(getResourceList("1", "2Gi"), getResourceList("", "")))
- // unset the namespace
- newPod.ObjectMeta.Namespace = ""
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Got unexpected error: %v", err)
- }
- if newPod.Namespace != namespace {
- t.Errorf("Got unexpected pod namespace: %q != %q", newPod.Namespace, namespace)
- }
- }
- // TestAdmitRejectsNegativeUsage verifies that usage for any measured resource cannot be negative.
- func TestAdmitRejectsNegativeUsage(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePersistentVolumeClaims: resource.MustParse("3"),
- corev1.ResourceRequestsStorage: resource.MustParse("100Gi"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePersistentVolumeClaims: resource.MustParse("1"),
- corev1.ResourceRequestsStorage: resource.MustParse("10Gi"),
- },
- },
- }
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- // verify quota rejects negative pvc storage requests
- newPvc := validPersistentVolumeClaim("not-allowed-pvc", getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("-1Gi")}, api.ResourceList{}))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPvc, nil, api.Kind("PersistentVolumeClaim").WithVersion("version"), newPvc.Namespace, newPvc.Name, corev1.Resource("persistentvolumeclaims").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err == nil {
- t.Errorf("Expected an error because the pvc has negative storage usage")
- }
- // verify quota accepts non-negative pvc storage requests
- newPvc = validPersistentVolumeClaim("not-allowed-pvc", getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("1Gi")}, api.ResourceList{}))
- err = handler.Validate(context.TODO(), admission.NewAttributesRecord(newPvc, nil, api.Kind("PersistentVolumeClaim").WithVersion("version"), newPvc.Namespace, newPvc.Name, corev1.Resource("persistentvolumeclaims").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- }
- // TestAdmitWhenUnrelatedResourceExceedsQuota verifies that if resource X exceeds quota, it does not prohibit resource Y from admission.
- func TestAdmitWhenUnrelatedResourceExceedsQuota(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceServices: resource.MustParse("3"),
- corev1.ResourcePods: resource.MustParse("4"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceServices: resource.MustParse("4"),
- corev1.ResourcePods: resource.MustParse("1"),
- },
- },
- }
- stopCh := make(chan struct{})
- defer close(stopCh)
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- config := &resourcequotaapi.Configuration{}
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
- // create a pod that should pass existing quota
- newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Unexpected error: %v", err)
- }
- }
- // TestAdmitLimitedResourceNoQuota verifies if a limited resource is configured with no quota, it cannot be consumed.
- func TestAdmitLimitedResourceNoQuota(t *testing.T) {
- kubeClient := fake.NewSimpleClientset()
- stopCh := make(chan struct{})
- defer close(stopCh)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- // disable consumption of cpu unless there is a covering quota.
- config := &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchContains: []string{"cpu"},
- },
- },
- }
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err == nil {
- t.Errorf("Expected an error for consuming a limited resource without quota.")
- }
- }
- // TestAdmitLimitedResourceNoQuotaIgnoresNonMatchingResources shows it ignores non matching resources in config.
- func TestAdmitLimitedResourceNoQuotaIgnoresNonMatchingResources(t *testing.T) {
- kubeClient := fake.NewSimpleClientset()
- stopCh := make(chan struct{})
- defer close(stopCh)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- // disable consumption of cpu unless there is a covering quota.
- config := &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "services",
- MatchContains: []string{"services"},
- },
- },
- }
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Fatalf("Unexpected error: %v", err)
- }
- }
- // TestAdmitLimitedResourceWithQuota verifies if a limited resource is configured with quota, it can be consumed.
- func TestAdmitLimitedResourceWithQuota(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceRequestsCPU: resource.MustParse("10"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceRequestsCPU: resource.MustParse("1"),
- },
- },
- }
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
- stopCh := make(chan struct{})
- defer close(stopCh)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- // disable consumption of cpu unless there is a covering quota.
- // disable consumption of cpu unless there is a covering quota.
- config := &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchContains: []string{"requests.cpu"}, // match on "requests.cpu" only
- },
- },
- }
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- indexer.Add(resourceQuota)
- newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- }
- // TestAdmitLimitedResourceWithMultipleQuota verifies if a limited resource is configured with quota, it can be consumed if one matches.
- func TestAdmitLimitedResourceWithMultipleQuota(t *testing.T) {
- resourceQuota1 := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota1", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceRequestsCPU: resource.MustParse("10"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceRequestsCPU: resource.MustParse("1"),
- },
- },
- }
- resourceQuota2 := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota2", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceMemory: resource.MustParse("10Gi"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceMemory: resource.MustParse("1Gi"),
- },
- },
- }
- kubeClient := fake.NewSimpleClientset(resourceQuota1, resourceQuota2)
- indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
- stopCh := make(chan struct{})
- defer close(stopCh)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- // disable consumption of cpu unless there is a covering quota.
- // disable consumption of cpu unless there is a covering quota.
- config := &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchContains: []string{"requests.cpu"}, // match on "requests.cpu" only
- },
- },
- }
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- indexer.Add(resourceQuota1)
- indexer.Add(resourceQuota2)
- newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- }
- // TestAdmitLimitedResourceWithQuotaThatDoesNotCover verifies if a limited resource is configured the quota must cover the resource.
- func TestAdmitLimitedResourceWithQuotaThatDoesNotCover(t *testing.T) {
- resourceQuota := &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceMemory: resource.MustParse("10Gi"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceMemory: resource.MustParse("1Gi"),
- },
- },
- }
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
- stopCh := make(chan struct{})
- defer close(stopCh)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- // disable consumption of cpu unless there is a covering quota.
- // disable consumption of cpu unless there is a covering quota.
- config := &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchContains: []string{"cpu"}, // match on "cpu" only
- },
- },
- }
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- indexer.Add(resourceQuota)
- newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if err == nil {
- t.Fatalf("Expected an error since the quota did not cover cpu")
- }
- }
- // TestAdmitLimitedScopeWithQuota verifies if a limited scope is configured the quota must cover the resource.
- func TestAdmitLimitedScopeWithCoverQuota(t *testing.T) {
- testCases := []struct {
- description string
- testPod *api.Pod
- quota *corev1.ResourceQuota
- anotherQuota *corev1.ResourceQuota
- config *resourcequotaapi.Configuration
- expErr string
- }{
- {
- description: "Covering quota exists for configured limited scope PriorityClassNameExists.",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- ScopeSelector: &corev1.ScopeSelector{
- MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpExists},
- },
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- },
- },
- expErr: "",
- },
- {
- description: "configured limited scope PriorityClassNameExists and limited cpu resource. No covering quota for cpu and pod admit fails.",
- testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- ScopeSelector: &corev1.ScopeSelector{
- MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpExists},
- },
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- MatchContains: []string{"requests.cpu"}, // match on "requests.cpu" only
- },
- },
- },
- expErr: "insufficient quota to consume: requests.cpu",
- },
- {
- description: "Covering quota does not exist for configured limited scope PriorityClassNameExists.",
- testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
- quota: &corev1.ResourceQuota{},
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- },
- },
- expErr: "insufficient quota to match these scopes: [{PriorityClass Exists []}]",
- },
- {
- description: "Covering quota does not exist for configured limited scope resourceQuotaBestEffort",
- testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "fake-priority"),
- quota: &corev1.ResourceQuota{},
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopeBestEffort,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- },
- },
- expErr: "insufficient quota to match these scopes: [{BestEffort Exists []}]",
- },
- {
- description: "Covering quota exist for configured limited scope resourceQuotaBestEffort",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "fake-priority"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeBestEffort},
- },
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopeBestEffort,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- },
- },
- expErr: "",
- },
- {
- description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Neither matches pod. Pod allowed",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", "")), "fake-priority"),
- quota: &corev1.ResourceQuota{},
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopeBestEffort,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- expErr: "",
- },
- {
- description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Only BestEffort scope matches pod. Pod admit fails because covering quota is missing for BestEffort scope",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "fake-priority"),
- quota: &corev1.ResourceQuota{},
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopeBestEffort,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- expErr: "insufficient quota to match these scopes: [{BestEffort Exists []}]",
- },
- {
- description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Only PriorityClass scope matches pod. Pod admit fails because covering quota is missing for PriorityClass scope",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", "")), "cluster-services"),
- quota: &corev1.ResourceQuota{},
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopeBestEffort,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- expErr: "insufficient quota to match these scopes: [{PriorityClass In [cluster-services]}]",
- },
- {
- description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Both the scopes matches pod. Pod admit fails because covering quota is missing for PriorityClass scope and BestEffort scope",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
- quota: &corev1.ResourceQuota{},
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopeBestEffort,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- expErr: "insufficient quota to match these scopes: [{BestEffort Exists []} {PriorityClass In [cluster-services]}]",
- },
- {
- description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Both the scopes matches pod. Quota available only for BestEffort scope. Pod admit fails because covering quota is missing for PriorityClass scope",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeBestEffort},
- },
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopeBestEffort,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- expErr: "insufficient quota to match these scopes: [{PriorityClass In [cluster-services]}]",
- },
- {
- description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Both the scopes matches pod. Quota available only for PriorityClass scope. Pod admit fails because covering quota is missing for BestEffort scope",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- ScopeSelector: &corev1.ScopeSelector{
- MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopeBestEffort,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- expErr: "insufficient quota to match these scopes: [{BestEffort Exists []}]",
- },
- {
- description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Both the scopes matches pod. Quota available only for both the scopes. Pod admit success. No Error",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeBestEffort},
- },
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("5"),
- },
- Used: corev1.ResourceList{
- corev1.ResourcePods: resource.MustParse("3"),
- },
- },
- },
- anotherQuota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- ScopeSelector: &corev1.ScopeSelector{
- MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopeBestEffort,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- expErr: "",
- },
- {
- description: "Pod allowed with priorityclass if limited scope PriorityClassNameExists not configured.",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
- quota: &corev1.ResourceQuota{},
- config: &resourcequotaapi.Configuration{},
- expErr: "",
- },
- {
- description: "quota fails, though covering quota for configured limited scope PriorityClassNameExists exists.",
- testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "20Gi"), getResourceList("", "")), "fake-priority"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- ScopeSelector: &corev1.ScopeSelector{
- MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpExists},
- },
- },
- },
- Status: corev1.ResourceQuotaStatus{
- Hard: corev1.ResourceList{
- corev1.ResourceMemory: resource.MustParse("10Gi"),
- },
- Used: corev1.ResourceList{
- corev1.ResourceMemory: resource.MustParse("1Gi"),
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpExists,
- },
- },
- },
- },
- },
- expErr: "forbidden: exceeded quota: quota, requested: memory=20Gi, used: memory=1Gi, limited: memory=10Gi",
- },
- {
- description: "Pod has different priorityclass than configured limited. Covering quota exists for configured limited scope PriorityClassIn.",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- ScopeSelector: &corev1.ScopeSelector{
- MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- expErr: "",
- },
- {
- description: "Pod has limited priorityclass. Covering quota exists for configured limited scope PriorityClassIn.",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- ScopeSelector: &corev1.ScopeSelector{
- MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"cluster-services"},
- },
- },
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"another-priorityclass-name", "cluster-services"},
- },
- },
- },
- },
- },
- expErr: "",
- },
- {
- description: "Pod has limited priorityclass. Covering quota does not exist for configured limited scope PriorityClassIn.",
- testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- ScopeSelector: &corev1.ScopeSelector{
- MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"another-priorityclass-name"},
- },
- },
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"another-priorityclass-name", "cluster-services"},
- },
- },
- },
- },
- },
- expErr: "insufficient quota to match these scopes: [{PriorityClass In [another-priorityclass-name cluster-services]}]",
- },
- {
- description: "From the above test case, just changing pod priority from cluster-services to another-priorityclass-name. expecting no error",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "another-priorityclass-name"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- ScopeSelector: &corev1.ScopeSelector{
- MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"another-priorityclass-name"},
- },
- },
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"another-priorityclass-name", "cluster-services"},
- },
- },
- },
- },
- },
- expErr: "",
- },
- {
- description: "Pod has limited priorityclass. Covering quota does NOT exists for configured limited scope PriorityClassIn.",
- testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
- quota: &corev1.ResourceQuota{},
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"another-priorityclass-name", "cluster-services"},
- },
- },
- },
- },
- },
- expErr: "insufficient quota to match these scopes: [{PriorityClass In [another-priorityclass-name cluster-services]}]",
- },
- {
- description: "Pod has limited priorityclass. Covering quota exists for configured limited scope PriorityClassIn through PriorityClassNameExists",
- testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
- quota: &corev1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
- Spec: corev1.ResourceQuotaSpec{
- ScopeSelector: &corev1.ScopeSelector{
- MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpExists},
- },
- },
- },
- },
- config: &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "pods",
- MatchScopes: []corev1.ScopedResourceSelectorRequirement{
- {
- ScopeName: corev1.ResourceQuotaScopePriorityClass,
- Operator: corev1.ScopeSelectorOpIn,
- Values: []string{"another-priorityclass-name", "cluster-services"},
- },
- },
- },
- },
- },
- expErr: "",
- },
- }
- for _, testCase := range testCases {
- newPod := testCase.testPod
- config := testCase.config
- resourceQuota := testCase.quota
- kubeClient := fake.NewSimpleClientset(resourceQuota)
- if testCase.anotherQuota != nil {
- kubeClient = fake.NewSimpleClientset(resourceQuota, testCase.anotherQuota)
- }
- indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
- stopCh := make(chan struct{})
- defer close(stopCh)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- indexer.Add(resourceQuota)
- if testCase.anotherQuota != nil {
- indexer.Add(testCase.anotherQuota)
- }
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
- if testCase.expErr == "" {
- if err != nil {
- t.Fatalf("Testcase, %v, failed with unexpected error: %v. ExpErr: %v", testCase.description, err, testCase.expErr)
- }
- } else {
- if !strings.Contains(fmt.Sprintf("%v", err), testCase.expErr) {
- t.Fatalf("Testcase, %v, failed with unexpected error: %v. ExpErr: %v", testCase.description, err, testCase.expErr)
- }
- }
- }
- }
- // TestAdmitZeroDeltaUsageWithoutCoveringQuota verifies that resource quota is not required for zero delta requests.
- func TestAdmitZeroDeltaUsageWithoutCoveringQuota(t *testing.T) {
- kubeClient := fake.NewSimpleClientset()
- stopCh := make(chan struct{})
- defer close(stopCh)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- // disable services unless there is a covering quota.
- config := &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "services",
- MatchContains: []string{"services.loadbalancers"},
- },
- },
- }
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- existingService := &api.Service{
- ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test", ResourceVersion: "1"},
- Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
- }
- newService := &api.Service{
- ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test"},
- Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
- }
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newService, existingService, api.Kind("Service").WithVersion("version"), newService.Namespace, newService.Name, corev1.Resource("services").WithVersion("version"), "", admission.Update, &metav1.CreateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- }
- // TestAdmitRejectIncreaseUsageWithoutCoveringQuota verifies that resource quota is required for delta requests that increase usage.
- func TestAdmitRejectIncreaseUsageWithoutCoveringQuota(t *testing.T) {
- kubeClient := fake.NewSimpleClientset()
- stopCh := make(chan struct{})
- defer close(stopCh)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- // disable services unless there is a covering quota.
- config := &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "services",
- MatchContains: []string{"services.loadbalancers"},
- },
- },
- }
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- existingService := &api.Service{
- ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test", ResourceVersion: "1"},
- Spec: api.ServiceSpec{
- Type: api.ServiceTypeNodePort,
- Ports: []api.ServicePort{{Port: 1234}},
- },
- }
- newService := &api.Service{
- ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test"},
- Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
- }
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newService, existingService, api.Kind("Service").WithVersion("version"), newService.Namespace, newService.Name, corev1.Resource("services").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
- if err == nil {
- t.Errorf("Expected an error for consuming a limited resource without quota.")
- }
- }
- // TestAdmitAllowDecreaseUsageWithoutCoveringQuota verifies that resource quota is not required for delta requests that decrease usage.
- func TestAdmitAllowDecreaseUsageWithoutCoveringQuota(t *testing.T) {
- kubeClient := fake.NewSimpleClientset()
- stopCh := make(chan struct{})
- defer close(stopCh)
- informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
- quotaAccessor, _ := newQuotaAccessor()
- quotaAccessor.client = kubeClient
- quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
- // disable services unless there is a covering quota.
- config := &resourcequotaapi.Configuration{
- LimitedResources: []resourcequotaapi.LimitedResource{
- {
- Resource: "services",
- MatchContains: []string{"services.loadbalancers"},
- },
- },
- }
- quotaConfiguration := install.NewQuotaConfigurationForAdmission()
- evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
- handler := &QuotaAdmission{
- Handler: admission.NewHandler(admission.Create, admission.Update),
- evaluator: evaluator,
- }
- existingService := &api.Service{
- ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test", ResourceVersion: "1"},
- Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
- }
- newService := &api.Service{
- ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test"},
- Spec: api.ServiceSpec{
- Type: api.ServiceTypeNodePort,
- Ports: []api.ServicePort{{Port: 1234}},
- },
- }
- err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newService, existingService, api.Kind("Service").WithVersion("version"), newService.Namespace, newService.Name, corev1.Resource("services").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
- if err != nil {
- t.Errorf("Expected no error for decreasing a limited resource without quota, got %v", err)
- }
- }
|