12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321 |
- /*
- 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 (
- "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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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)
- }
- }
|