admission_test.go 97 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322
  1. /*
  2. Copyright 2014 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package resourcequota
  14. import (
  15. "context"
  16. "fmt"
  17. "strconv"
  18. "strings"
  19. "testing"
  20. "time"
  21. lru "github.com/hashicorp/golang-lru"
  22. corev1 "k8s.io/api/core/v1"
  23. "k8s.io/apimachinery/pkg/api/resource"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. "k8s.io/apimachinery/pkg/util/sets"
  26. "k8s.io/apiserver/pkg/admission"
  27. utilfeature "k8s.io/apiserver/pkg/util/feature"
  28. "k8s.io/client-go/informers"
  29. "k8s.io/client-go/kubernetes/fake"
  30. testcore "k8s.io/client-go/testing"
  31. "k8s.io/client-go/tools/cache"
  32. featuregatetesting "k8s.io/component-base/featuregate/testing"
  33. api "k8s.io/kubernetes/pkg/apis/core"
  34. "k8s.io/kubernetes/pkg/controller"
  35. "k8s.io/kubernetes/pkg/features"
  36. "k8s.io/kubernetes/pkg/quota/v1/generic"
  37. "k8s.io/kubernetes/pkg/quota/v1/install"
  38. resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
  39. )
  40. func getResourceList(cpu, memory string) api.ResourceList {
  41. res := api.ResourceList{}
  42. if cpu != "" {
  43. res[api.ResourceCPU] = resource.MustParse(cpu)
  44. }
  45. if memory != "" {
  46. res[api.ResourceMemory] = resource.MustParse(memory)
  47. }
  48. return res
  49. }
  50. func getResourceRequirements(requests, limits api.ResourceList) api.ResourceRequirements {
  51. res := api.ResourceRequirements{}
  52. res.Requests = requests
  53. res.Limits = limits
  54. return res
  55. }
  56. func validPod(name string, numContainers int, resources api.ResourceRequirements) *api.Pod {
  57. pod := &api.Pod{
  58. ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"},
  59. Spec: api.PodSpec{},
  60. }
  61. pod.Spec.Containers = make([]api.Container, 0, numContainers)
  62. for i := 0; i < numContainers; i++ {
  63. pod.Spec.Containers = append(pod.Spec.Containers, api.Container{
  64. Image: "foo:V" + strconv.Itoa(i),
  65. Resources: resources,
  66. })
  67. }
  68. return pod
  69. }
  70. func validPodWithPriority(name string, numContainers int, resources api.ResourceRequirements, priorityClass string) *api.Pod {
  71. pod := validPod(name, numContainers, resources)
  72. if priorityClass != "" {
  73. pod.Spec.PriorityClassName = priorityClass
  74. }
  75. return pod
  76. }
  77. func validPersistentVolumeClaim(name string, resources api.ResourceRequirements) *api.PersistentVolumeClaim {
  78. return &api.PersistentVolumeClaim{
  79. ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"},
  80. Spec: api.PersistentVolumeClaimSpec{
  81. Resources: resources,
  82. },
  83. }
  84. }
  85. func TestPrettyPrint(t *testing.T) {
  86. toResourceList := func(resources map[corev1.ResourceName]string) corev1.ResourceList {
  87. resourceList := corev1.ResourceList{}
  88. for key, value := range resources {
  89. resourceList[key] = resource.MustParse(value)
  90. }
  91. return resourceList
  92. }
  93. testCases := []struct {
  94. input corev1.ResourceList
  95. expected string
  96. }{
  97. {
  98. input: toResourceList(map[corev1.ResourceName]string{
  99. corev1.ResourceCPU: "100m",
  100. }),
  101. expected: "cpu=100m",
  102. },
  103. {
  104. input: toResourceList(map[corev1.ResourceName]string{
  105. corev1.ResourcePods: "10",
  106. corev1.ResourceServices: "10",
  107. corev1.ResourceReplicationControllers: "10",
  108. corev1.ResourceServicesNodePorts: "10",
  109. corev1.ResourceRequestsCPU: "100m",
  110. corev1.ResourceRequestsMemory: "100Mi",
  111. corev1.ResourceLimitsCPU: "100m",
  112. corev1.ResourceLimitsMemory: "100Mi",
  113. }),
  114. expected: "limits.cpu=100m,limits.memory=100Mi,pods=10,replicationcontrollers=10,requests.cpu=100m,requests.memory=100Mi,services=10,services.nodeports=10",
  115. },
  116. }
  117. for i, testCase := range testCases {
  118. result := prettyPrint(testCase.input)
  119. if result != testCase.expected {
  120. t.Errorf("Pretty print did not give stable sorted output[%d], expected %v, but got %v", i, testCase.expected, result)
  121. }
  122. }
  123. }
  124. // TestAdmissionIgnoresDelete verifies that the admission controller ignores delete operations
  125. func TestAdmissionIgnoresDelete(t *testing.T) {
  126. stopCh := make(chan struct{})
  127. defer close(stopCh)
  128. kubeClient := fake.NewSimpleClientset()
  129. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  130. quotaAccessor, _ := newQuotaAccessor()
  131. quotaAccessor.client = kubeClient
  132. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  133. config := &resourcequotaapi.Configuration{}
  134. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  135. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  136. handler := &QuotaAdmission{
  137. Handler: admission.NewHandler(admission.Create, admission.Update),
  138. evaluator: evaluator,
  139. }
  140. namespace := "default"
  141. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(nil, nil, api.Kind("Pod").WithVersion("version"), namespace, "name", corev1.Resource("pods").WithVersion("version"), "", admission.Delete, &metav1.DeleteOptions{}, false, nil), nil)
  142. if err != nil {
  143. t.Errorf("ResourceQuota should admit all deletes: %v", err)
  144. }
  145. }
  146. // TestAdmissionIgnoresSubresources verifies that the admission controller ignores subresources
  147. // It verifies that creation of a pod that would have exceeded quota is properly failed
  148. // It verifies that create operations to a subresource that would have exceeded quota would succeed
  149. func TestAdmissionIgnoresSubresources(t *testing.T) {
  150. resourceQuota := &corev1.ResourceQuota{}
  151. resourceQuota.Name = "quota"
  152. resourceQuota.Namespace = "test"
  153. resourceQuota.Status = corev1.ResourceQuotaStatus{
  154. Hard: corev1.ResourceList{},
  155. Used: corev1.ResourceList{},
  156. }
  157. resourceQuota.Status.Hard[corev1.ResourceMemory] = resource.MustParse("2Gi")
  158. resourceQuota.Status.Used[corev1.ResourceMemory] = resource.MustParse("1Gi")
  159. stopCh := make(chan struct{})
  160. defer close(stopCh)
  161. kubeClient := fake.NewSimpleClientset()
  162. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  163. quotaAccessor, _ := newQuotaAccessor()
  164. quotaAccessor.client = kubeClient
  165. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  166. config := &resourcequotaapi.Configuration{}
  167. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  168. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  169. handler := &QuotaAdmission{
  170. Handler: admission.NewHandler(admission.Create, admission.Update),
  171. evaluator: evaluator,
  172. }
  173. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  174. newPod := validPod("123", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("", "")))
  175. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  176. if err == nil {
  177. t.Errorf("Expected an error because the pod exceeded allowed quota")
  178. }
  179. err = handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "subresource", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  180. if err != nil {
  181. t.Errorf("Did not expect an error because the action went to a subresource: %v", err)
  182. }
  183. }
  184. // TestAdmitBelowQuotaLimit verifies that a pod when created has its usage reflected on the quota
  185. func TestAdmitBelowQuotaLimit(t *testing.T) {
  186. resourceQuota := &corev1.ResourceQuota{
  187. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  188. Status: corev1.ResourceQuotaStatus{
  189. Hard: corev1.ResourceList{
  190. corev1.ResourceCPU: resource.MustParse("3"),
  191. corev1.ResourceMemory: resource.MustParse("100Gi"),
  192. corev1.ResourcePods: resource.MustParse("5"),
  193. },
  194. Used: corev1.ResourceList{
  195. corev1.ResourceCPU: resource.MustParse("1"),
  196. corev1.ResourceMemory: resource.MustParse("50Gi"),
  197. corev1.ResourcePods: resource.MustParse("3"),
  198. },
  199. },
  200. }
  201. stopCh := make(chan struct{})
  202. defer close(stopCh)
  203. kubeClient := fake.NewSimpleClientset(resourceQuota)
  204. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  205. quotaAccessor, _ := newQuotaAccessor()
  206. quotaAccessor.client = kubeClient
  207. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  208. config := &resourcequotaapi.Configuration{}
  209. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  210. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  211. handler := &QuotaAdmission{
  212. Handler: admission.NewHandler(admission.Create, admission.Update),
  213. evaluator: evaluator,
  214. }
  215. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  216. newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("", "")))
  217. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  218. if err != nil {
  219. t.Errorf("Unexpected error: %v", err)
  220. }
  221. if len(kubeClient.Actions()) == 0 {
  222. t.Errorf("Expected a client action")
  223. }
  224. expectedActionSet := sets.NewString(
  225. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  226. )
  227. actionSet := sets.NewString()
  228. for _, action := range kubeClient.Actions() {
  229. actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
  230. }
  231. if !actionSet.HasAll(expectedActionSet.List()...) {
  232. t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
  233. }
  234. decimatedActions := removeListWatch(kubeClient.Actions())
  235. lastActionIndex := len(decimatedActions) - 1
  236. usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
  237. expectedUsage := corev1.ResourceQuota{
  238. Status: corev1.ResourceQuotaStatus{
  239. Hard: corev1.ResourceList{
  240. corev1.ResourceCPU: resource.MustParse("3"),
  241. corev1.ResourceMemory: resource.MustParse("100Gi"),
  242. corev1.ResourcePods: resource.MustParse("5"),
  243. },
  244. Used: corev1.ResourceList{
  245. corev1.ResourceCPU: resource.MustParse("1100m"),
  246. corev1.ResourceMemory: resource.MustParse("52Gi"),
  247. corev1.ResourcePods: resource.MustParse("4"),
  248. },
  249. },
  250. }
  251. for k, v := range expectedUsage.Status.Used {
  252. actual := usage.Status.Used[k]
  253. actualValue := actual.String()
  254. expectedValue := v.String()
  255. if expectedValue != actualValue {
  256. t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
  257. }
  258. }
  259. }
  260. // TestAdmitDryRun verifies that a pod when created with dry-run doesn not have its usage reflected on the quota
  261. // and that dry-run requests can still be rejected if they would exceed the quota
  262. func TestAdmitDryRun(t *testing.T) {
  263. resourceQuota := &corev1.ResourceQuota{
  264. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  265. Status: corev1.ResourceQuotaStatus{
  266. Hard: corev1.ResourceList{
  267. corev1.ResourceCPU: resource.MustParse("3"),
  268. corev1.ResourceMemory: resource.MustParse("100Gi"),
  269. corev1.ResourcePods: resource.MustParse("5"),
  270. },
  271. Used: corev1.ResourceList{
  272. corev1.ResourceCPU: resource.MustParse("1"),
  273. corev1.ResourceMemory: resource.MustParse("50Gi"),
  274. corev1.ResourcePods: resource.MustParse("3"),
  275. },
  276. },
  277. }
  278. stopCh := make(chan struct{})
  279. defer close(stopCh)
  280. kubeClient := fake.NewSimpleClientset(resourceQuota)
  281. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  282. quotaAccessor, _ := newQuotaAccessor()
  283. quotaAccessor.client = kubeClient
  284. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  285. config := &resourcequotaapi.Configuration{}
  286. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  287. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  288. handler := &QuotaAdmission{
  289. Handler: admission.NewHandler(admission.Create, admission.Update),
  290. evaluator: evaluator,
  291. }
  292. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  293. newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("", "")))
  294. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, true, nil), nil)
  295. if err != nil {
  296. t.Errorf("Unexpected error: %v", err)
  297. }
  298. newPod = validPod("too-large-pod", 1, getResourceRequirements(getResourceList("100m", "60Gi"), getResourceList("", "")))
  299. err = handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, true, nil), nil)
  300. if err == nil {
  301. t.Errorf("Expected error but got none")
  302. }
  303. if len(kubeClient.Actions()) != 0 {
  304. t.Errorf("Expected no client action on dry-run")
  305. }
  306. }
  307. // TestAdmitHandlesOldObjects verifies that admit handles updates correctly with old objects
  308. func TestAdmitHandlesOldObjects(t *testing.T) {
  309. // in this scenario, the old quota was based on a service type=loadbalancer
  310. resourceQuota := &corev1.ResourceQuota{
  311. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  312. Status: corev1.ResourceQuotaStatus{
  313. Hard: corev1.ResourceList{
  314. corev1.ResourceServices: resource.MustParse("10"),
  315. corev1.ResourceServicesLoadBalancers: resource.MustParse("10"),
  316. corev1.ResourceServicesNodePorts: resource.MustParse("10"),
  317. },
  318. Used: corev1.ResourceList{
  319. corev1.ResourceServices: resource.MustParse("1"),
  320. corev1.ResourceServicesLoadBalancers: resource.MustParse("1"),
  321. corev1.ResourceServicesNodePorts: resource.MustParse("0"),
  322. },
  323. },
  324. }
  325. // start up quota system
  326. stopCh := make(chan struct{})
  327. defer close(stopCh)
  328. kubeClient := fake.NewSimpleClientset(resourceQuota)
  329. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  330. quotaAccessor, _ := newQuotaAccessor()
  331. quotaAccessor.client = kubeClient
  332. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  333. config := &resourcequotaapi.Configuration{}
  334. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  335. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  336. handler := &QuotaAdmission{
  337. Handler: admission.NewHandler(admission.Create, admission.Update),
  338. evaluator: evaluator,
  339. }
  340. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  341. // old service was a load balancer, but updated version is a node port.
  342. existingService := &api.Service{
  343. ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test", ResourceVersion: "1"},
  344. Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
  345. }
  346. newService := &api.Service{
  347. ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test"},
  348. Spec: api.ServiceSpec{
  349. Type: api.ServiceTypeNodePort,
  350. Ports: []api.ServicePort{{Port: 1234}},
  351. },
  352. }
  353. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newService, existingService, api.Kind("Service").WithVersion("version"), newService.Namespace, newService.Name, corev1.Resource("services").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
  354. if err != nil {
  355. t.Errorf("Unexpected error: %v", err)
  356. }
  357. if len(kubeClient.Actions()) == 0 {
  358. t.Errorf("Expected a client action")
  359. }
  360. // the only action should have been to update the quota (since we should not have fetched the previous item)
  361. expectedActionSet := sets.NewString(
  362. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  363. )
  364. actionSet := sets.NewString()
  365. for _, action := range kubeClient.Actions() {
  366. actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
  367. }
  368. if !actionSet.HasAll(expectedActionSet.List()...) {
  369. t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
  370. }
  371. // verify usage decremented the loadbalancer, and incremented the nodeport, but kept the service the same.
  372. decimatedActions := removeListWatch(kubeClient.Actions())
  373. lastActionIndex := len(decimatedActions) - 1
  374. usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
  375. // Verify service usage. Since we don't add negative values, the corev1.ResourceServicesLoadBalancers
  376. // will remain on last reported value
  377. expectedUsage := corev1.ResourceQuota{
  378. Status: corev1.ResourceQuotaStatus{
  379. Hard: corev1.ResourceList{
  380. corev1.ResourceServices: resource.MustParse("10"),
  381. corev1.ResourceServicesLoadBalancers: resource.MustParse("10"),
  382. corev1.ResourceServicesNodePorts: resource.MustParse("10"),
  383. },
  384. Used: corev1.ResourceList{
  385. corev1.ResourceServices: resource.MustParse("1"),
  386. corev1.ResourceServicesLoadBalancers: resource.MustParse("1"),
  387. corev1.ResourceServicesNodePorts: resource.MustParse("1"),
  388. },
  389. },
  390. }
  391. for k, v := range expectedUsage.Status.Used {
  392. actual := usage.Status.Used[k]
  393. actualValue := actual.String()
  394. expectedValue := v.String()
  395. if expectedValue != actualValue {
  396. t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
  397. }
  398. }
  399. }
  400. func TestAdmitHandlesNegativePVCUpdates(t *testing.T) {
  401. resourceQuota := &corev1.ResourceQuota{
  402. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  403. Status: corev1.ResourceQuotaStatus{
  404. Hard: corev1.ResourceList{
  405. corev1.ResourcePersistentVolumeClaims: resource.MustParse("3"),
  406. corev1.ResourceRequestsStorage: resource.MustParse("100Gi"),
  407. },
  408. Used: corev1.ResourceList{
  409. corev1.ResourcePersistentVolumeClaims: resource.MustParse("1"),
  410. corev1.ResourceRequestsStorage: resource.MustParse("10Gi"),
  411. },
  412. },
  413. }
  414. // start up quota system
  415. stopCh := make(chan struct{})
  416. defer close(stopCh)
  417. defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExpandPersistentVolumes, true)()
  418. kubeClient := fake.NewSimpleClientset(resourceQuota)
  419. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  420. quotaAccessor, _ := newQuotaAccessor()
  421. quotaAccessor.client = kubeClient
  422. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  423. config := &resourcequotaapi.Configuration{}
  424. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  425. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  426. handler := &QuotaAdmission{
  427. Handler: admission.NewHandler(admission.Create, admission.Update),
  428. evaluator: evaluator,
  429. }
  430. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  431. oldPVC := &api.PersistentVolumeClaim{
  432. ObjectMeta: metav1.ObjectMeta{Name: "pvc-to-update", Namespace: "test", ResourceVersion: "1"},
  433. Spec: api.PersistentVolumeClaimSpec{
  434. Resources: getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("10Gi")}, api.ResourceList{}),
  435. },
  436. }
  437. newPVC := &api.PersistentVolumeClaim{
  438. ObjectMeta: metav1.ObjectMeta{Name: "pvc-to-update", Namespace: "test"},
  439. Spec: api.PersistentVolumeClaimSpec{
  440. Resources: getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("5Gi")}, api.ResourceList{}),
  441. },
  442. }
  443. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPVC, oldPVC, api.Kind("PersistentVolumeClaim").WithVersion("version"), newPVC.Namespace, newPVC.Name, corev1.Resource("persistentvolumeclaims").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
  444. if err != nil {
  445. t.Errorf("Unexpected error: %v", err)
  446. }
  447. if len(kubeClient.Actions()) != 0 {
  448. t.Errorf("No client action should be taken in case of negative updates")
  449. }
  450. }
  451. func TestAdmitHandlesPVCUpdates(t *testing.T) {
  452. resourceQuota := &corev1.ResourceQuota{
  453. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  454. Status: corev1.ResourceQuotaStatus{
  455. Hard: corev1.ResourceList{
  456. corev1.ResourcePersistentVolumeClaims: resource.MustParse("3"),
  457. corev1.ResourceRequestsStorage: resource.MustParse("100Gi"),
  458. },
  459. Used: corev1.ResourceList{
  460. corev1.ResourcePersistentVolumeClaims: resource.MustParse("1"),
  461. corev1.ResourceRequestsStorage: resource.MustParse("10Gi"),
  462. },
  463. },
  464. }
  465. defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExpandPersistentVolumes, true)()
  466. // start up quota system
  467. stopCh := make(chan struct{})
  468. defer close(stopCh)
  469. kubeClient := fake.NewSimpleClientset(resourceQuota)
  470. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  471. quotaAccessor, _ := newQuotaAccessor()
  472. quotaAccessor.client = kubeClient
  473. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  474. config := &resourcequotaapi.Configuration{}
  475. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  476. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  477. handler := &QuotaAdmission{
  478. Handler: admission.NewHandler(admission.Create, admission.Update),
  479. evaluator: evaluator,
  480. }
  481. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  482. oldPVC := &api.PersistentVolumeClaim{
  483. ObjectMeta: metav1.ObjectMeta{Name: "pvc-to-update", Namespace: "test", ResourceVersion: "1"},
  484. Spec: api.PersistentVolumeClaimSpec{
  485. Resources: getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("10Gi")}, api.ResourceList{}),
  486. },
  487. }
  488. newPVC := &api.PersistentVolumeClaim{
  489. ObjectMeta: metav1.ObjectMeta{Name: "pvc-to-update", Namespace: "test"},
  490. Spec: api.PersistentVolumeClaimSpec{
  491. Resources: getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("15Gi")}, api.ResourceList{}),
  492. },
  493. }
  494. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPVC, oldPVC, api.Kind("PersistentVolumeClaim").WithVersion("version"), newPVC.Namespace, newPVC.Name, corev1.Resource("persistentvolumeclaims").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
  495. if err != nil {
  496. t.Errorf("Unexpected error: %v", err)
  497. }
  498. if len(kubeClient.Actions()) == 0 {
  499. t.Errorf("Expected a client action")
  500. }
  501. // the only action should have been to update the quota (since we should not have fetched the previous item)
  502. expectedActionSet := sets.NewString(
  503. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  504. )
  505. actionSet := sets.NewString()
  506. for _, action := range kubeClient.Actions() {
  507. actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
  508. }
  509. if !actionSet.HasAll(expectedActionSet.List()...) {
  510. t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
  511. }
  512. decimatedActions := removeListWatch(kubeClient.Actions())
  513. lastActionIndex := len(decimatedActions) - 1
  514. usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
  515. expectedUsage := corev1.ResourceQuota{
  516. Status: corev1.ResourceQuotaStatus{
  517. Hard: corev1.ResourceList{
  518. corev1.ResourcePersistentVolumeClaims: resource.MustParse("3"),
  519. corev1.ResourceRequestsStorage: resource.MustParse("100Gi"),
  520. },
  521. Used: corev1.ResourceList{
  522. corev1.ResourcePersistentVolumeClaims: resource.MustParse("1"),
  523. corev1.ResourceRequestsStorage: resource.MustParse("15Gi"),
  524. },
  525. },
  526. }
  527. for k, v := range expectedUsage.Status.Used {
  528. actual := usage.Status.Used[k]
  529. actualValue := actual.String()
  530. expectedValue := v.String()
  531. if expectedValue != actualValue {
  532. t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
  533. }
  534. }
  535. }
  536. // TestAdmitHandlesCreatingUpdates verifies that admit handles updates which behave as creates
  537. func TestAdmitHandlesCreatingUpdates(t *testing.T) {
  538. // in this scenario, there is an existing service
  539. resourceQuota := &corev1.ResourceQuota{
  540. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  541. Status: corev1.ResourceQuotaStatus{
  542. Hard: corev1.ResourceList{
  543. corev1.ResourceServices: resource.MustParse("10"),
  544. corev1.ResourceServicesLoadBalancers: resource.MustParse("10"),
  545. corev1.ResourceServicesNodePorts: resource.MustParse("10"),
  546. },
  547. Used: corev1.ResourceList{
  548. corev1.ResourceServices: resource.MustParse("1"),
  549. corev1.ResourceServicesLoadBalancers: resource.MustParse("1"),
  550. corev1.ResourceServicesNodePorts: resource.MustParse("0"),
  551. },
  552. },
  553. }
  554. // start up quota system
  555. stopCh := make(chan struct{})
  556. defer close(stopCh)
  557. kubeClient := fake.NewSimpleClientset(resourceQuota)
  558. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  559. quotaAccessor, _ := newQuotaAccessor()
  560. quotaAccessor.client = kubeClient
  561. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  562. config := &resourcequotaapi.Configuration{}
  563. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  564. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  565. handler := &QuotaAdmission{
  566. Handler: admission.NewHandler(admission.Create, admission.Update),
  567. evaluator: evaluator,
  568. }
  569. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  570. // old service didn't exist, so this update is actually a create
  571. oldService := &api.Service{
  572. ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test", ResourceVersion: ""},
  573. Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
  574. }
  575. newService := &api.Service{
  576. ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test"},
  577. Spec: api.ServiceSpec{
  578. Type: api.ServiceTypeNodePort,
  579. Ports: []api.ServicePort{{Port: 1234}},
  580. },
  581. }
  582. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newService, oldService, api.Kind("Service").WithVersion("version"), newService.Namespace, newService.Name, corev1.Resource("services").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
  583. if err != nil {
  584. t.Errorf("Unexpected error: %v", err)
  585. }
  586. if len(kubeClient.Actions()) == 0 {
  587. t.Errorf("Expected a client action")
  588. }
  589. // the only action should have been to update the quota (since we should not have fetched the previous item)
  590. expectedActionSet := sets.NewString(
  591. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  592. )
  593. actionSet := sets.NewString()
  594. for _, action := range kubeClient.Actions() {
  595. actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
  596. }
  597. if !actionSet.HasAll(expectedActionSet.List()...) {
  598. t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
  599. }
  600. // verify that the "old" object was ignored for calculating the new usage
  601. decimatedActions := removeListWatch(kubeClient.Actions())
  602. lastActionIndex := len(decimatedActions) - 1
  603. usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
  604. expectedUsage := corev1.ResourceQuota{
  605. Status: corev1.ResourceQuotaStatus{
  606. Hard: corev1.ResourceList{
  607. corev1.ResourceServices: resource.MustParse("10"),
  608. corev1.ResourceServicesLoadBalancers: resource.MustParse("10"),
  609. corev1.ResourceServicesNodePorts: resource.MustParse("10"),
  610. },
  611. Used: corev1.ResourceList{
  612. corev1.ResourceServices: resource.MustParse("2"),
  613. corev1.ResourceServicesLoadBalancers: resource.MustParse("1"),
  614. corev1.ResourceServicesNodePorts: resource.MustParse("1"),
  615. },
  616. },
  617. }
  618. for k, v := range expectedUsage.Status.Used {
  619. actual := usage.Status.Used[k]
  620. actualValue := actual.String()
  621. expectedValue := v.String()
  622. if expectedValue != actualValue {
  623. t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
  624. }
  625. }
  626. }
  627. // TestAdmitExceedQuotaLimit verifies that if a pod exceeded allowed usage that its rejected during admission.
  628. func TestAdmitExceedQuotaLimit(t *testing.T) {
  629. resourceQuota := &corev1.ResourceQuota{
  630. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  631. Status: corev1.ResourceQuotaStatus{
  632. Hard: corev1.ResourceList{
  633. corev1.ResourceCPU: resource.MustParse("3"),
  634. corev1.ResourceMemory: resource.MustParse("100Gi"),
  635. corev1.ResourcePods: resource.MustParse("5"),
  636. },
  637. Used: corev1.ResourceList{
  638. corev1.ResourceCPU: resource.MustParse("1"),
  639. corev1.ResourceMemory: resource.MustParse("50Gi"),
  640. corev1.ResourcePods: resource.MustParse("3"),
  641. },
  642. },
  643. }
  644. stopCh := make(chan struct{})
  645. defer close(stopCh)
  646. kubeClient := fake.NewSimpleClientset(resourceQuota)
  647. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  648. quotaAccessor, _ := newQuotaAccessor()
  649. quotaAccessor.client = kubeClient
  650. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  651. config := &resourcequotaapi.Configuration{}
  652. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  653. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  654. handler := &QuotaAdmission{
  655. Handler: admission.NewHandler(admission.Create, admission.Update),
  656. evaluator: evaluator,
  657. }
  658. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  659. newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
  660. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  661. if err == nil {
  662. t.Errorf("Expected an error exceeding quota")
  663. }
  664. }
  665. // TestAdmitEnforceQuotaConstraints verifies that if a quota tracks a particular resource that that resource is
  666. // specified on the pod. In this case, we create a quota that tracks cpu request, memory request, and memory limit.
  667. // We ensure that a pod that does not specify a memory limit that it fails in admission.
  668. func TestAdmitEnforceQuotaConstraints(t *testing.T) {
  669. resourceQuota := &corev1.ResourceQuota{
  670. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  671. Status: corev1.ResourceQuotaStatus{
  672. Hard: corev1.ResourceList{
  673. corev1.ResourceCPU: resource.MustParse("3"),
  674. corev1.ResourceMemory: resource.MustParse("100Gi"),
  675. corev1.ResourceLimitsMemory: resource.MustParse("200Gi"),
  676. corev1.ResourcePods: resource.MustParse("5"),
  677. },
  678. Used: corev1.ResourceList{
  679. corev1.ResourceCPU: resource.MustParse("1"),
  680. corev1.ResourceMemory: resource.MustParse("50Gi"),
  681. corev1.ResourceLimitsMemory: resource.MustParse("100Gi"),
  682. corev1.ResourcePods: resource.MustParse("3"),
  683. },
  684. },
  685. }
  686. stopCh := make(chan struct{})
  687. defer close(stopCh)
  688. kubeClient := fake.NewSimpleClientset(resourceQuota)
  689. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  690. quotaAccessor, _ := newQuotaAccessor()
  691. quotaAccessor.client = kubeClient
  692. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  693. config := &resourcequotaapi.Configuration{}
  694. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  695. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  696. handler := &QuotaAdmission{
  697. Handler: admission.NewHandler(admission.Create, admission.Update),
  698. evaluator: evaluator,
  699. }
  700. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  701. // verify all values are specified as required on the quota
  702. newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("200m", "")))
  703. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  704. if err == nil {
  705. t.Errorf("Expected an error because the pod does not specify a memory limit")
  706. }
  707. }
  708. // TestAdmitPodInNamespaceWithoutQuota ensures that if a namespace has no quota, that a pod can get in
  709. func TestAdmitPodInNamespaceWithoutQuota(t *testing.T) {
  710. resourceQuota := &corev1.ResourceQuota{
  711. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "other", ResourceVersion: "124"},
  712. Status: corev1.ResourceQuotaStatus{
  713. Hard: corev1.ResourceList{
  714. corev1.ResourceCPU: resource.MustParse("3"),
  715. corev1.ResourceMemory: resource.MustParse("100Gi"),
  716. corev1.ResourceLimitsMemory: resource.MustParse("200Gi"),
  717. corev1.ResourcePods: resource.MustParse("5"),
  718. },
  719. Used: corev1.ResourceList{
  720. corev1.ResourceCPU: resource.MustParse("1"),
  721. corev1.ResourceMemory: resource.MustParse("50Gi"),
  722. corev1.ResourceLimitsMemory: resource.MustParse("100Gi"),
  723. corev1.ResourcePods: resource.MustParse("3"),
  724. },
  725. },
  726. }
  727. liveLookupCache, err := lru.New(100)
  728. if err != nil {
  729. t.Fatal(err)
  730. }
  731. stopCh := make(chan struct{})
  732. defer close(stopCh)
  733. kubeClient := fake.NewSimpleClientset(resourceQuota)
  734. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  735. quotaAccessor, _ := newQuotaAccessor()
  736. quotaAccessor.client = kubeClient
  737. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  738. quotaAccessor.liveLookupCache = liveLookupCache
  739. config := &resourcequotaapi.Configuration{}
  740. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  741. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  742. handler := &QuotaAdmission{
  743. Handler: admission.NewHandler(admission.Create, admission.Update),
  744. evaluator: evaluator,
  745. }
  746. // Add to the index
  747. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  748. newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("200m", "")))
  749. // Add to the lru cache so we do not do a live client lookup
  750. liveLookupCache.Add(newPod.Namespace, liveLookupEntry{expiry: time.Now().Add(time.Duration(30 * time.Second)), items: []*corev1.ResourceQuota{}})
  751. err = handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  752. if err != nil {
  753. t.Errorf("Did not expect an error because the pod is in a different namespace than the quota")
  754. }
  755. }
  756. // TestAdmitBelowTerminatingQuotaLimit ensures that terminating pods are charged to the right quota.
  757. // It creates a terminating and non-terminating quota, and creates a terminating pod.
  758. // It ensures that the terminating quota is incremented, and the non-terminating quota is not.
  759. func TestAdmitBelowTerminatingQuotaLimit(t *testing.T) {
  760. resourceQuotaNonTerminating := &corev1.ResourceQuota{
  761. ObjectMeta: metav1.ObjectMeta{Name: "quota-non-terminating", Namespace: "test", ResourceVersion: "124"},
  762. Spec: corev1.ResourceQuotaSpec{
  763. Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeNotTerminating},
  764. },
  765. Status: corev1.ResourceQuotaStatus{
  766. Hard: corev1.ResourceList{
  767. corev1.ResourceCPU: resource.MustParse("3"),
  768. corev1.ResourceMemory: resource.MustParse("100Gi"),
  769. corev1.ResourcePods: resource.MustParse("5"),
  770. },
  771. Used: corev1.ResourceList{
  772. corev1.ResourceCPU: resource.MustParse("1"),
  773. corev1.ResourceMemory: resource.MustParse("50Gi"),
  774. corev1.ResourcePods: resource.MustParse("3"),
  775. },
  776. },
  777. }
  778. resourceQuotaTerminating := &corev1.ResourceQuota{
  779. ObjectMeta: metav1.ObjectMeta{Name: "quota-terminating", Namespace: "test", ResourceVersion: "124"},
  780. Spec: corev1.ResourceQuotaSpec{
  781. Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeTerminating},
  782. },
  783. Status: corev1.ResourceQuotaStatus{
  784. Hard: corev1.ResourceList{
  785. corev1.ResourceCPU: resource.MustParse("3"),
  786. corev1.ResourceMemory: resource.MustParse("100Gi"),
  787. corev1.ResourcePods: resource.MustParse("5"),
  788. },
  789. Used: corev1.ResourceList{
  790. corev1.ResourceCPU: resource.MustParse("1"),
  791. corev1.ResourceMemory: resource.MustParse("50Gi"),
  792. corev1.ResourcePods: resource.MustParse("3"),
  793. },
  794. },
  795. }
  796. stopCh := make(chan struct{})
  797. defer close(stopCh)
  798. kubeClient := fake.NewSimpleClientset(resourceQuotaTerminating, resourceQuotaNonTerminating)
  799. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  800. quotaAccessor, _ := newQuotaAccessor()
  801. quotaAccessor.client = kubeClient
  802. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  803. config := &resourcequotaapi.Configuration{}
  804. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  805. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  806. handler := &QuotaAdmission{
  807. Handler: admission.NewHandler(admission.Create, admission.Update),
  808. evaluator: evaluator,
  809. }
  810. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuotaNonTerminating)
  811. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuotaTerminating)
  812. // create a pod that has an active deadline
  813. newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("", "")))
  814. activeDeadlineSeconds := int64(30)
  815. newPod.Spec.ActiveDeadlineSeconds = &activeDeadlineSeconds
  816. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  817. if err != nil {
  818. t.Errorf("Unexpected error: %v", err)
  819. }
  820. if len(kubeClient.Actions()) == 0 {
  821. t.Errorf("Expected a client action")
  822. }
  823. expectedActionSet := sets.NewString(
  824. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  825. )
  826. actionSet := sets.NewString()
  827. for _, action := range kubeClient.Actions() {
  828. actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
  829. }
  830. if !actionSet.HasAll(expectedActionSet.List()...) {
  831. t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
  832. }
  833. decimatedActions := removeListWatch(kubeClient.Actions())
  834. lastActionIndex := len(decimatedActions) - 1
  835. usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
  836. // ensure only the quota-terminating was updated
  837. if usage.Name != resourceQuotaTerminating.Name {
  838. t.Errorf("Incremented the wrong quota, expected %v, actual %v", resourceQuotaTerminating.Name, usage.Name)
  839. }
  840. expectedUsage := corev1.ResourceQuota{
  841. Status: corev1.ResourceQuotaStatus{
  842. Hard: corev1.ResourceList{
  843. corev1.ResourceCPU: resource.MustParse("3"),
  844. corev1.ResourceMemory: resource.MustParse("100Gi"),
  845. corev1.ResourcePods: resource.MustParse("5"),
  846. },
  847. Used: corev1.ResourceList{
  848. corev1.ResourceCPU: resource.MustParse("1100m"),
  849. corev1.ResourceMemory: resource.MustParse("52Gi"),
  850. corev1.ResourcePods: resource.MustParse("4"),
  851. },
  852. },
  853. }
  854. for k, v := range expectedUsage.Status.Used {
  855. actual := usage.Status.Used[k]
  856. actualValue := actual.String()
  857. expectedValue := v.String()
  858. if expectedValue != actualValue {
  859. t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
  860. }
  861. }
  862. }
  863. // TestAdmitBelowBestEffortQuotaLimit creates a best effort and non-best effort quota.
  864. // It verifies that best effort pods are properly scoped to the best effort quota document.
  865. func TestAdmitBelowBestEffortQuotaLimit(t *testing.T) {
  866. resourceQuotaBestEffort := &corev1.ResourceQuota{
  867. ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
  868. Spec: corev1.ResourceQuotaSpec{
  869. Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeBestEffort},
  870. },
  871. Status: corev1.ResourceQuotaStatus{
  872. Hard: corev1.ResourceList{
  873. corev1.ResourcePods: resource.MustParse("5"),
  874. },
  875. Used: corev1.ResourceList{
  876. corev1.ResourcePods: resource.MustParse("3"),
  877. },
  878. },
  879. }
  880. resourceQuotaNotBestEffort := &corev1.ResourceQuota{
  881. ObjectMeta: metav1.ObjectMeta{Name: "quota-not-besteffort", Namespace: "test", ResourceVersion: "124"},
  882. Spec: corev1.ResourceQuotaSpec{
  883. Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeNotBestEffort},
  884. },
  885. Status: corev1.ResourceQuotaStatus{
  886. Hard: corev1.ResourceList{
  887. corev1.ResourcePods: resource.MustParse("5"),
  888. },
  889. Used: corev1.ResourceList{
  890. corev1.ResourcePods: resource.MustParse("3"),
  891. },
  892. },
  893. }
  894. stopCh := make(chan struct{})
  895. defer close(stopCh)
  896. kubeClient := fake.NewSimpleClientset(resourceQuotaBestEffort, resourceQuotaNotBestEffort)
  897. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  898. quotaAccessor, _ := newQuotaAccessor()
  899. quotaAccessor.client = kubeClient
  900. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  901. config := &resourcequotaapi.Configuration{}
  902. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  903. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  904. handler := &QuotaAdmission{
  905. Handler: admission.NewHandler(admission.Create, admission.Update),
  906. evaluator: evaluator,
  907. }
  908. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuotaBestEffort)
  909. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuotaNotBestEffort)
  910. // create a pod that is best effort because it does not make a request for anything
  911. newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")))
  912. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  913. if err != nil {
  914. t.Errorf("Unexpected error: %v", err)
  915. }
  916. expectedActionSet := sets.NewString(
  917. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  918. )
  919. actionSet := sets.NewString()
  920. for _, action := range kubeClient.Actions() {
  921. actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
  922. }
  923. if !actionSet.HasAll(expectedActionSet.List()...) {
  924. t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
  925. }
  926. decimatedActions := removeListWatch(kubeClient.Actions())
  927. lastActionIndex := len(decimatedActions) - 1
  928. usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*corev1.ResourceQuota)
  929. if usage.Name != resourceQuotaBestEffort.Name {
  930. t.Errorf("Incremented the wrong quota, expected %v, actual %v", resourceQuotaBestEffort.Name, usage.Name)
  931. }
  932. expectedUsage := corev1.ResourceQuota{
  933. Status: corev1.ResourceQuotaStatus{
  934. Hard: corev1.ResourceList{
  935. corev1.ResourcePods: resource.MustParse("5"),
  936. },
  937. Used: corev1.ResourceList{
  938. corev1.ResourcePods: resource.MustParse("4"),
  939. },
  940. },
  941. }
  942. for k, v := range expectedUsage.Status.Used {
  943. actual := usage.Status.Used[k]
  944. actualValue := actual.String()
  945. expectedValue := v.String()
  946. if expectedValue != actualValue {
  947. t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue)
  948. }
  949. }
  950. }
  951. func removeListWatch(in []testcore.Action) []testcore.Action {
  952. decimatedActions := []testcore.Action{}
  953. // list and watch resource quota is done to maintain our cache, so that's expected. Remove them from results
  954. for i := range in {
  955. if in[i].Matches("list", "resourcequotas") || in[i].Matches("watch", "resourcequotas") {
  956. continue
  957. }
  958. decimatedActions = append(decimatedActions, in[i])
  959. }
  960. return decimatedActions
  961. }
  962. // TestAdmitBestEffortQuotaLimitIgnoresBurstable validates that a besteffort quota does not match a resource
  963. // guaranteed pod.
  964. func TestAdmitBestEffortQuotaLimitIgnoresBurstable(t *testing.T) {
  965. resourceQuota := &corev1.ResourceQuota{
  966. ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
  967. Spec: corev1.ResourceQuotaSpec{
  968. Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeBestEffort},
  969. },
  970. Status: corev1.ResourceQuotaStatus{
  971. Hard: corev1.ResourceList{
  972. corev1.ResourcePods: resource.MustParse("5"),
  973. },
  974. Used: corev1.ResourceList{
  975. corev1.ResourcePods: resource.MustParse("3"),
  976. },
  977. },
  978. }
  979. stopCh := make(chan struct{})
  980. defer close(stopCh)
  981. kubeClient := fake.NewSimpleClientset(resourceQuota)
  982. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  983. quotaAccessor, _ := newQuotaAccessor()
  984. quotaAccessor.client = kubeClient
  985. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  986. config := &resourcequotaapi.Configuration{}
  987. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  988. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  989. handler := &QuotaAdmission{
  990. Handler: admission.NewHandler(admission.Create, admission.Update),
  991. evaluator: evaluator,
  992. }
  993. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  994. newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", "")))
  995. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  996. if err != nil {
  997. t.Errorf("Unexpected error: %v", err)
  998. }
  999. decimatedActions := removeListWatch(kubeClient.Actions())
  1000. if len(decimatedActions) != 0 {
  1001. t.Errorf("Expected no client actions because the incoming pod did not match best effort quota: %v", kubeClient.Actions())
  1002. }
  1003. }
  1004. func TestHasUsageStats(t *testing.T) {
  1005. testCases := map[string]struct {
  1006. a corev1.ResourceQuota
  1007. relevant []corev1.ResourceName
  1008. expected bool
  1009. }{
  1010. "empty": {
  1011. a: corev1.ResourceQuota{Status: corev1.ResourceQuotaStatus{Hard: corev1.ResourceList{}}},
  1012. relevant: []corev1.ResourceName{corev1.ResourceMemory},
  1013. expected: true,
  1014. },
  1015. "hard-only": {
  1016. a: corev1.ResourceQuota{
  1017. Status: corev1.ResourceQuotaStatus{
  1018. Hard: corev1.ResourceList{
  1019. corev1.ResourceMemory: resource.MustParse("1Gi"),
  1020. },
  1021. Used: corev1.ResourceList{},
  1022. },
  1023. },
  1024. relevant: []corev1.ResourceName{corev1.ResourceMemory},
  1025. expected: false,
  1026. },
  1027. "hard-used": {
  1028. a: corev1.ResourceQuota{
  1029. Status: corev1.ResourceQuotaStatus{
  1030. Hard: corev1.ResourceList{
  1031. corev1.ResourceMemory: resource.MustParse("1Gi"),
  1032. },
  1033. Used: corev1.ResourceList{
  1034. corev1.ResourceMemory: resource.MustParse("500Mi"),
  1035. },
  1036. },
  1037. },
  1038. relevant: []corev1.ResourceName{corev1.ResourceMemory},
  1039. expected: true,
  1040. },
  1041. "hard-used-relevant": {
  1042. a: corev1.ResourceQuota{
  1043. Status: corev1.ResourceQuotaStatus{
  1044. Hard: corev1.ResourceList{
  1045. corev1.ResourceMemory: resource.MustParse("1Gi"),
  1046. corev1.ResourcePods: resource.MustParse("1"),
  1047. },
  1048. Used: corev1.ResourceList{
  1049. corev1.ResourceMemory: resource.MustParse("500Mi"),
  1050. },
  1051. },
  1052. },
  1053. relevant: []corev1.ResourceName{corev1.ResourceMemory},
  1054. expected: true,
  1055. },
  1056. }
  1057. for testName, testCase := range testCases {
  1058. if result := hasUsageStats(&testCase.a, testCase.relevant); result != testCase.expected {
  1059. t.Errorf("%s expected: %v, actual: %v", testName, testCase.expected, result)
  1060. }
  1061. }
  1062. }
  1063. // TestAdmissionSetsMissingNamespace verifies that if an object lacks a
  1064. // namespace, it will be set.
  1065. func TestAdmissionSetsMissingNamespace(t *testing.T) {
  1066. namespace := "test"
  1067. resourceQuota := &corev1.ResourceQuota{
  1068. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: namespace, ResourceVersion: "124"},
  1069. Status: corev1.ResourceQuotaStatus{
  1070. Hard: corev1.ResourceList{
  1071. corev1.ResourcePods: resource.MustParse("3"),
  1072. },
  1073. Used: corev1.ResourceList{
  1074. corev1.ResourcePods: resource.MustParse("1"),
  1075. },
  1076. },
  1077. }
  1078. stopCh := make(chan struct{})
  1079. defer close(stopCh)
  1080. kubeClient := fake.NewSimpleClientset(resourceQuota)
  1081. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  1082. quotaAccessor, _ := newQuotaAccessor()
  1083. quotaAccessor.client = kubeClient
  1084. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  1085. config := &resourcequotaapi.Configuration{}
  1086. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  1087. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  1088. handler := &QuotaAdmission{
  1089. Handler: admission.NewHandler(admission.Create, admission.Update),
  1090. evaluator: evaluator,
  1091. }
  1092. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  1093. newPod := validPod("pod-without-namespace", 1, getResourceRequirements(getResourceList("1", "2Gi"), getResourceList("", "")))
  1094. // unset the namespace
  1095. newPod.ObjectMeta.Namespace = ""
  1096. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  1097. if err != nil {
  1098. t.Errorf("Got unexpected error: %v", err)
  1099. }
  1100. if newPod.Namespace != namespace {
  1101. t.Errorf("Got unexpected pod namespace: %q != %q", newPod.Namespace, namespace)
  1102. }
  1103. }
  1104. // TestAdmitRejectsNegativeUsage verifies that usage for any measured resource cannot be negative.
  1105. func TestAdmitRejectsNegativeUsage(t *testing.T) {
  1106. resourceQuota := &corev1.ResourceQuota{
  1107. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1108. Status: corev1.ResourceQuotaStatus{
  1109. Hard: corev1.ResourceList{
  1110. corev1.ResourcePersistentVolumeClaims: resource.MustParse("3"),
  1111. corev1.ResourceRequestsStorage: resource.MustParse("100Gi"),
  1112. },
  1113. Used: corev1.ResourceList{
  1114. corev1.ResourcePersistentVolumeClaims: resource.MustParse("1"),
  1115. corev1.ResourceRequestsStorage: resource.MustParse("10Gi"),
  1116. },
  1117. },
  1118. }
  1119. stopCh := make(chan struct{})
  1120. defer close(stopCh)
  1121. kubeClient := fake.NewSimpleClientset(resourceQuota)
  1122. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  1123. quotaAccessor, _ := newQuotaAccessor()
  1124. quotaAccessor.client = kubeClient
  1125. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  1126. config := &resourcequotaapi.Configuration{}
  1127. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  1128. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  1129. handler := &QuotaAdmission{
  1130. Handler: admission.NewHandler(admission.Create, admission.Update),
  1131. evaluator: evaluator,
  1132. }
  1133. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  1134. // verify quota rejects negative pvc storage requests
  1135. newPvc := validPersistentVolumeClaim("not-allowed-pvc", getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("-1Gi")}, api.ResourceList{}))
  1136. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPvc, nil, api.Kind("PersistentVolumeClaim").WithVersion("version"), newPvc.Namespace, newPvc.Name, corev1.Resource("persistentvolumeclaims").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  1137. if err == nil {
  1138. t.Errorf("Expected an error because the pvc has negative storage usage")
  1139. }
  1140. // verify quota accepts non-negative pvc storage requests
  1141. newPvc = validPersistentVolumeClaim("not-allowed-pvc", getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("1Gi")}, api.ResourceList{}))
  1142. err = handler.Validate(context.TODO(), admission.NewAttributesRecord(newPvc, nil, api.Kind("PersistentVolumeClaim").WithVersion("version"), newPvc.Namespace, newPvc.Name, corev1.Resource("persistentvolumeclaims").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  1143. if err != nil {
  1144. t.Errorf("Unexpected error: %v", err)
  1145. }
  1146. }
  1147. // TestAdmitWhenUnrelatedResourceExceedsQuota verifies that if resource X exceeds quota, it does not prohibit resource Y from admission.
  1148. func TestAdmitWhenUnrelatedResourceExceedsQuota(t *testing.T) {
  1149. resourceQuota := &corev1.ResourceQuota{
  1150. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1151. Status: corev1.ResourceQuotaStatus{
  1152. Hard: corev1.ResourceList{
  1153. corev1.ResourceServices: resource.MustParse("3"),
  1154. corev1.ResourcePods: resource.MustParse("4"),
  1155. },
  1156. Used: corev1.ResourceList{
  1157. corev1.ResourceServices: resource.MustParse("4"),
  1158. corev1.ResourcePods: resource.MustParse("1"),
  1159. },
  1160. },
  1161. }
  1162. stopCh := make(chan struct{})
  1163. defer close(stopCh)
  1164. kubeClient := fake.NewSimpleClientset(resourceQuota)
  1165. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  1166. quotaAccessor, _ := newQuotaAccessor()
  1167. quotaAccessor.client = kubeClient
  1168. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  1169. config := &resourcequotaapi.Configuration{}
  1170. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  1171. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  1172. handler := &QuotaAdmission{
  1173. Handler: admission.NewHandler(admission.Create, admission.Update),
  1174. evaluator: evaluator,
  1175. }
  1176. informerFactory.Core().V1().ResourceQuotas().Informer().GetIndexer().Add(resourceQuota)
  1177. // create a pod that should pass existing quota
  1178. newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")))
  1179. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  1180. if err != nil {
  1181. t.Errorf("Unexpected error: %v", err)
  1182. }
  1183. }
  1184. // TestAdmitLimitedResourceNoQuota verifies if a limited resource is configured with no quota, it cannot be consumed.
  1185. func TestAdmitLimitedResourceNoQuota(t *testing.T) {
  1186. kubeClient := fake.NewSimpleClientset()
  1187. stopCh := make(chan struct{})
  1188. defer close(stopCh)
  1189. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  1190. quotaAccessor, _ := newQuotaAccessor()
  1191. quotaAccessor.client = kubeClient
  1192. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  1193. // disable consumption of cpu unless there is a covering quota.
  1194. config := &resourcequotaapi.Configuration{
  1195. LimitedResources: []resourcequotaapi.LimitedResource{
  1196. {
  1197. Resource: "pods",
  1198. MatchContains: []string{"cpu"},
  1199. },
  1200. },
  1201. }
  1202. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  1203. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  1204. handler := &QuotaAdmission{
  1205. Handler: admission.NewHandler(admission.Create, admission.Update),
  1206. evaluator: evaluator,
  1207. }
  1208. newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
  1209. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  1210. if err == nil {
  1211. t.Errorf("Expected an error for consuming a limited resource without quota.")
  1212. }
  1213. }
  1214. // TestAdmitLimitedResourceNoQuotaIgnoresNonMatchingResources shows it ignores non matching resources in config.
  1215. func TestAdmitLimitedResourceNoQuotaIgnoresNonMatchingResources(t *testing.T) {
  1216. kubeClient := fake.NewSimpleClientset()
  1217. stopCh := make(chan struct{})
  1218. defer close(stopCh)
  1219. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  1220. quotaAccessor, _ := newQuotaAccessor()
  1221. quotaAccessor.client = kubeClient
  1222. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  1223. // disable consumption of cpu unless there is a covering quota.
  1224. config := &resourcequotaapi.Configuration{
  1225. LimitedResources: []resourcequotaapi.LimitedResource{
  1226. {
  1227. Resource: "services",
  1228. MatchContains: []string{"services"},
  1229. },
  1230. },
  1231. }
  1232. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  1233. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  1234. handler := &QuotaAdmission{
  1235. Handler: admission.NewHandler(admission.Create, admission.Update),
  1236. evaluator: evaluator,
  1237. }
  1238. newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
  1239. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  1240. if err != nil {
  1241. t.Fatalf("Unexpected error: %v", err)
  1242. }
  1243. }
  1244. // TestAdmitLimitedResourceWithQuota verifies if a limited resource is configured with quota, it can be consumed.
  1245. func TestAdmitLimitedResourceWithQuota(t *testing.T) {
  1246. resourceQuota := &corev1.ResourceQuota{
  1247. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1248. Status: corev1.ResourceQuotaStatus{
  1249. Hard: corev1.ResourceList{
  1250. corev1.ResourceRequestsCPU: resource.MustParse("10"),
  1251. },
  1252. Used: corev1.ResourceList{
  1253. corev1.ResourceRequestsCPU: resource.MustParse("1"),
  1254. },
  1255. },
  1256. }
  1257. kubeClient := fake.NewSimpleClientset(resourceQuota)
  1258. indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
  1259. stopCh := make(chan struct{})
  1260. defer close(stopCh)
  1261. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  1262. quotaAccessor, _ := newQuotaAccessor()
  1263. quotaAccessor.client = kubeClient
  1264. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  1265. // disable consumption of cpu unless there is a covering quota.
  1266. // disable consumption of cpu unless there is a covering quota.
  1267. config := &resourcequotaapi.Configuration{
  1268. LimitedResources: []resourcequotaapi.LimitedResource{
  1269. {
  1270. Resource: "pods",
  1271. MatchContains: []string{"requests.cpu"}, // match on "requests.cpu" only
  1272. },
  1273. },
  1274. }
  1275. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  1276. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  1277. handler := &QuotaAdmission{
  1278. Handler: admission.NewHandler(admission.Create, admission.Update),
  1279. evaluator: evaluator,
  1280. }
  1281. indexer.Add(resourceQuota)
  1282. newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
  1283. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  1284. if err != nil {
  1285. t.Errorf("unexpected error: %v", err)
  1286. }
  1287. }
  1288. // TestAdmitLimitedResourceWithMultipleQuota verifies if a limited resource is configured with quota, it can be consumed if one matches.
  1289. func TestAdmitLimitedResourceWithMultipleQuota(t *testing.T) {
  1290. resourceQuota1 := &corev1.ResourceQuota{
  1291. ObjectMeta: metav1.ObjectMeta{Name: "quota1", Namespace: "test", ResourceVersion: "124"},
  1292. Status: corev1.ResourceQuotaStatus{
  1293. Hard: corev1.ResourceList{
  1294. corev1.ResourceRequestsCPU: resource.MustParse("10"),
  1295. },
  1296. Used: corev1.ResourceList{
  1297. corev1.ResourceRequestsCPU: resource.MustParse("1"),
  1298. },
  1299. },
  1300. }
  1301. resourceQuota2 := &corev1.ResourceQuota{
  1302. ObjectMeta: metav1.ObjectMeta{Name: "quota2", Namespace: "test", ResourceVersion: "124"},
  1303. Status: corev1.ResourceQuotaStatus{
  1304. Hard: corev1.ResourceList{
  1305. corev1.ResourceMemory: resource.MustParse("10Gi"),
  1306. },
  1307. Used: corev1.ResourceList{
  1308. corev1.ResourceMemory: resource.MustParse("1Gi"),
  1309. },
  1310. },
  1311. }
  1312. kubeClient := fake.NewSimpleClientset(resourceQuota1, resourceQuota2)
  1313. indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
  1314. stopCh := make(chan struct{})
  1315. defer close(stopCh)
  1316. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  1317. quotaAccessor, _ := newQuotaAccessor()
  1318. quotaAccessor.client = kubeClient
  1319. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  1320. // disable consumption of cpu unless there is a covering quota.
  1321. // disable consumption of cpu unless there is a covering quota.
  1322. config := &resourcequotaapi.Configuration{
  1323. LimitedResources: []resourcequotaapi.LimitedResource{
  1324. {
  1325. Resource: "pods",
  1326. MatchContains: []string{"requests.cpu"}, // match on "requests.cpu" only
  1327. },
  1328. },
  1329. }
  1330. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  1331. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  1332. handler := &QuotaAdmission{
  1333. Handler: admission.NewHandler(admission.Create, admission.Update),
  1334. evaluator: evaluator,
  1335. }
  1336. indexer.Add(resourceQuota1)
  1337. indexer.Add(resourceQuota2)
  1338. newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
  1339. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  1340. if err != nil {
  1341. t.Errorf("unexpected error: %v", err)
  1342. }
  1343. }
  1344. // TestAdmitLimitedResourceWithQuotaThatDoesNotCover verifies if a limited resource is configured the quota must cover the resource.
  1345. func TestAdmitLimitedResourceWithQuotaThatDoesNotCover(t *testing.T) {
  1346. resourceQuota := &corev1.ResourceQuota{
  1347. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1348. Status: corev1.ResourceQuotaStatus{
  1349. Hard: corev1.ResourceList{
  1350. corev1.ResourceMemory: resource.MustParse("10Gi"),
  1351. },
  1352. Used: corev1.ResourceList{
  1353. corev1.ResourceMemory: resource.MustParse("1Gi"),
  1354. },
  1355. },
  1356. }
  1357. kubeClient := fake.NewSimpleClientset(resourceQuota)
  1358. indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
  1359. stopCh := make(chan struct{})
  1360. defer close(stopCh)
  1361. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  1362. quotaAccessor, _ := newQuotaAccessor()
  1363. quotaAccessor.client = kubeClient
  1364. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  1365. // disable consumption of cpu unless there is a covering quota.
  1366. // disable consumption of cpu unless there is a covering quota.
  1367. config := &resourcequotaapi.Configuration{
  1368. LimitedResources: []resourcequotaapi.LimitedResource{
  1369. {
  1370. Resource: "pods",
  1371. MatchContains: []string{"cpu"}, // match on "cpu" only
  1372. },
  1373. },
  1374. }
  1375. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  1376. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  1377. handler := &QuotaAdmission{
  1378. Handler: admission.NewHandler(admission.Create, admission.Update),
  1379. evaluator: evaluator,
  1380. }
  1381. indexer.Add(resourceQuota)
  1382. newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
  1383. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  1384. if err == nil {
  1385. t.Fatalf("Expected an error since the quota did not cover cpu")
  1386. }
  1387. }
  1388. // TestAdmitLimitedScopeWithQuota verifies if a limited scope is configured the quota must cover the resource.
  1389. func TestAdmitLimitedScopeWithCoverQuota(t *testing.T) {
  1390. testCases := []struct {
  1391. description string
  1392. testPod *api.Pod
  1393. quota *corev1.ResourceQuota
  1394. anotherQuota *corev1.ResourceQuota
  1395. config *resourcequotaapi.Configuration
  1396. expErr string
  1397. }{
  1398. {
  1399. description: "Covering quota exists for configured limited scope PriorityClassNameExists.",
  1400. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
  1401. quota: &corev1.ResourceQuota{
  1402. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1403. Spec: corev1.ResourceQuotaSpec{
  1404. ScopeSelector: &corev1.ScopeSelector{
  1405. MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
  1406. {
  1407. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1408. Operator: corev1.ScopeSelectorOpExists},
  1409. },
  1410. },
  1411. },
  1412. },
  1413. config: &resourcequotaapi.Configuration{
  1414. LimitedResources: []resourcequotaapi.LimitedResource{
  1415. {
  1416. Resource: "pods",
  1417. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1418. {
  1419. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1420. Operator: corev1.ScopeSelectorOpExists,
  1421. },
  1422. },
  1423. },
  1424. },
  1425. },
  1426. expErr: "",
  1427. },
  1428. {
  1429. description: "configured limited scope PriorityClassNameExists and limited cpu resource. No covering quota for cpu and pod admit fails.",
  1430. testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
  1431. quota: &corev1.ResourceQuota{
  1432. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1433. Spec: corev1.ResourceQuotaSpec{
  1434. ScopeSelector: &corev1.ScopeSelector{
  1435. MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
  1436. {
  1437. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1438. Operator: corev1.ScopeSelectorOpExists},
  1439. },
  1440. },
  1441. },
  1442. },
  1443. config: &resourcequotaapi.Configuration{
  1444. LimitedResources: []resourcequotaapi.LimitedResource{
  1445. {
  1446. Resource: "pods",
  1447. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1448. {
  1449. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1450. Operator: corev1.ScopeSelectorOpExists,
  1451. },
  1452. },
  1453. MatchContains: []string{"requests.cpu"}, // match on "requests.cpu" only
  1454. },
  1455. },
  1456. },
  1457. expErr: "insufficient quota to consume: requests.cpu",
  1458. },
  1459. {
  1460. description: "Covering quota does not exist for configured limited scope PriorityClassNameExists.",
  1461. testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
  1462. quota: &corev1.ResourceQuota{},
  1463. config: &resourcequotaapi.Configuration{
  1464. LimitedResources: []resourcequotaapi.LimitedResource{
  1465. {
  1466. Resource: "pods",
  1467. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1468. {
  1469. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1470. Operator: corev1.ScopeSelectorOpExists,
  1471. },
  1472. },
  1473. },
  1474. },
  1475. },
  1476. expErr: "insufficient quota to match these scopes: [{PriorityClass Exists []}]",
  1477. },
  1478. {
  1479. description: "Covering quota does not exist for configured limited scope resourceQuotaBestEffort",
  1480. testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "fake-priority"),
  1481. quota: &corev1.ResourceQuota{},
  1482. config: &resourcequotaapi.Configuration{
  1483. LimitedResources: []resourcequotaapi.LimitedResource{
  1484. {
  1485. Resource: "pods",
  1486. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1487. {
  1488. ScopeName: corev1.ResourceQuotaScopeBestEffort,
  1489. Operator: corev1.ScopeSelectorOpExists,
  1490. },
  1491. },
  1492. },
  1493. },
  1494. },
  1495. expErr: "insufficient quota to match these scopes: [{BestEffort Exists []}]",
  1496. },
  1497. {
  1498. description: "Covering quota exist for configured limited scope resourceQuotaBestEffort",
  1499. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "fake-priority"),
  1500. quota: &corev1.ResourceQuota{
  1501. ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
  1502. Spec: corev1.ResourceQuotaSpec{
  1503. Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeBestEffort},
  1504. },
  1505. Status: corev1.ResourceQuotaStatus{
  1506. Hard: corev1.ResourceList{
  1507. corev1.ResourcePods: resource.MustParse("5"),
  1508. },
  1509. Used: corev1.ResourceList{
  1510. corev1.ResourcePods: resource.MustParse("3"),
  1511. },
  1512. },
  1513. },
  1514. config: &resourcequotaapi.Configuration{
  1515. LimitedResources: []resourcequotaapi.LimitedResource{
  1516. {
  1517. Resource: "pods",
  1518. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1519. {
  1520. ScopeName: corev1.ResourceQuotaScopeBestEffort,
  1521. Operator: corev1.ScopeSelectorOpExists,
  1522. },
  1523. },
  1524. },
  1525. },
  1526. },
  1527. expErr: "",
  1528. },
  1529. {
  1530. description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Neither matches pod. Pod allowed",
  1531. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", "")), "fake-priority"),
  1532. quota: &corev1.ResourceQuota{},
  1533. config: &resourcequotaapi.Configuration{
  1534. LimitedResources: []resourcequotaapi.LimitedResource{
  1535. {
  1536. Resource: "pods",
  1537. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1538. {
  1539. ScopeName: corev1.ResourceQuotaScopeBestEffort,
  1540. Operator: corev1.ScopeSelectorOpExists,
  1541. },
  1542. },
  1543. },
  1544. {
  1545. Resource: "pods",
  1546. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1547. {
  1548. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1549. Operator: corev1.ScopeSelectorOpIn,
  1550. Values: []string{"cluster-services"},
  1551. },
  1552. },
  1553. },
  1554. },
  1555. },
  1556. expErr: "",
  1557. },
  1558. {
  1559. 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",
  1560. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "fake-priority"),
  1561. quota: &corev1.ResourceQuota{},
  1562. config: &resourcequotaapi.Configuration{
  1563. LimitedResources: []resourcequotaapi.LimitedResource{
  1564. {
  1565. Resource: "pods",
  1566. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1567. {
  1568. ScopeName: corev1.ResourceQuotaScopeBestEffort,
  1569. Operator: corev1.ScopeSelectorOpExists,
  1570. },
  1571. },
  1572. },
  1573. {
  1574. Resource: "pods",
  1575. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1576. {
  1577. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1578. Operator: corev1.ScopeSelectorOpIn,
  1579. Values: []string{"cluster-services"},
  1580. },
  1581. },
  1582. },
  1583. },
  1584. },
  1585. expErr: "insufficient quota to match these scopes: [{BestEffort Exists []}]",
  1586. },
  1587. {
  1588. 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",
  1589. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", "")), "cluster-services"),
  1590. quota: &corev1.ResourceQuota{},
  1591. config: &resourcequotaapi.Configuration{
  1592. LimitedResources: []resourcequotaapi.LimitedResource{
  1593. {
  1594. Resource: "pods",
  1595. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1596. {
  1597. ScopeName: corev1.ResourceQuotaScopeBestEffort,
  1598. Operator: corev1.ScopeSelectorOpExists,
  1599. },
  1600. },
  1601. },
  1602. {
  1603. Resource: "pods",
  1604. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1605. {
  1606. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1607. Operator: corev1.ScopeSelectorOpIn,
  1608. Values: []string{"cluster-services"},
  1609. },
  1610. },
  1611. },
  1612. },
  1613. },
  1614. expErr: "insufficient quota to match these scopes: [{PriorityClass In [cluster-services]}]",
  1615. },
  1616. {
  1617. 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",
  1618. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
  1619. quota: &corev1.ResourceQuota{},
  1620. config: &resourcequotaapi.Configuration{
  1621. LimitedResources: []resourcequotaapi.LimitedResource{
  1622. {
  1623. Resource: "pods",
  1624. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1625. {
  1626. ScopeName: corev1.ResourceQuotaScopeBestEffort,
  1627. Operator: corev1.ScopeSelectorOpExists,
  1628. },
  1629. },
  1630. },
  1631. {
  1632. Resource: "pods",
  1633. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1634. {
  1635. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1636. Operator: corev1.ScopeSelectorOpIn,
  1637. Values: []string{"cluster-services"},
  1638. },
  1639. },
  1640. },
  1641. },
  1642. },
  1643. expErr: "insufficient quota to match these scopes: [{BestEffort Exists []} {PriorityClass In [cluster-services]}]",
  1644. },
  1645. {
  1646. 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",
  1647. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
  1648. quota: &corev1.ResourceQuota{
  1649. ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
  1650. Spec: corev1.ResourceQuotaSpec{
  1651. Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeBestEffort},
  1652. },
  1653. Status: corev1.ResourceQuotaStatus{
  1654. Hard: corev1.ResourceList{
  1655. corev1.ResourcePods: resource.MustParse("5"),
  1656. },
  1657. Used: corev1.ResourceList{
  1658. corev1.ResourcePods: resource.MustParse("3"),
  1659. },
  1660. },
  1661. },
  1662. config: &resourcequotaapi.Configuration{
  1663. LimitedResources: []resourcequotaapi.LimitedResource{
  1664. {
  1665. Resource: "pods",
  1666. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1667. {
  1668. ScopeName: corev1.ResourceQuotaScopeBestEffort,
  1669. Operator: corev1.ScopeSelectorOpExists,
  1670. },
  1671. },
  1672. },
  1673. {
  1674. Resource: "pods",
  1675. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1676. {
  1677. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1678. Operator: corev1.ScopeSelectorOpIn,
  1679. Values: []string{"cluster-services"},
  1680. },
  1681. },
  1682. },
  1683. },
  1684. },
  1685. expErr: "insufficient quota to match these scopes: [{PriorityClass In [cluster-services]}]",
  1686. },
  1687. {
  1688. 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",
  1689. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
  1690. quota: &corev1.ResourceQuota{
  1691. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1692. Spec: corev1.ResourceQuotaSpec{
  1693. ScopeSelector: &corev1.ScopeSelector{
  1694. MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
  1695. {
  1696. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1697. Operator: corev1.ScopeSelectorOpIn,
  1698. Values: []string{"cluster-services"},
  1699. },
  1700. },
  1701. },
  1702. },
  1703. },
  1704. config: &resourcequotaapi.Configuration{
  1705. LimitedResources: []resourcequotaapi.LimitedResource{
  1706. {
  1707. Resource: "pods",
  1708. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1709. {
  1710. ScopeName: corev1.ResourceQuotaScopeBestEffort,
  1711. Operator: corev1.ScopeSelectorOpExists,
  1712. },
  1713. },
  1714. },
  1715. {
  1716. Resource: "pods",
  1717. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1718. {
  1719. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1720. Operator: corev1.ScopeSelectorOpIn,
  1721. Values: []string{"cluster-services"},
  1722. },
  1723. },
  1724. },
  1725. },
  1726. },
  1727. expErr: "insufficient quota to match these scopes: [{BestEffort Exists []}]",
  1728. },
  1729. {
  1730. 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",
  1731. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
  1732. quota: &corev1.ResourceQuota{
  1733. ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
  1734. Spec: corev1.ResourceQuotaSpec{
  1735. Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeBestEffort},
  1736. },
  1737. Status: corev1.ResourceQuotaStatus{
  1738. Hard: corev1.ResourceList{
  1739. corev1.ResourcePods: resource.MustParse("5"),
  1740. },
  1741. Used: corev1.ResourceList{
  1742. corev1.ResourcePods: resource.MustParse("3"),
  1743. },
  1744. },
  1745. },
  1746. anotherQuota: &corev1.ResourceQuota{
  1747. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1748. Spec: corev1.ResourceQuotaSpec{
  1749. ScopeSelector: &corev1.ScopeSelector{
  1750. MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
  1751. {
  1752. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1753. Operator: corev1.ScopeSelectorOpIn,
  1754. Values: []string{"cluster-services"},
  1755. },
  1756. },
  1757. },
  1758. },
  1759. },
  1760. config: &resourcequotaapi.Configuration{
  1761. LimitedResources: []resourcequotaapi.LimitedResource{
  1762. {
  1763. Resource: "pods",
  1764. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1765. {
  1766. ScopeName: corev1.ResourceQuotaScopeBestEffort,
  1767. Operator: corev1.ScopeSelectorOpExists,
  1768. },
  1769. },
  1770. },
  1771. {
  1772. Resource: "pods",
  1773. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1774. {
  1775. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1776. Operator: corev1.ScopeSelectorOpIn,
  1777. Values: []string{"cluster-services"},
  1778. },
  1779. },
  1780. },
  1781. },
  1782. },
  1783. expErr: "",
  1784. },
  1785. {
  1786. description: "Pod allowed with priorityclass if limited scope PriorityClassNameExists not configured.",
  1787. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
  1788. quota: &corev1.ResourceQuota{},
  1789. config: &resourcequotaapi.Configuration{},
  1790. expErr: "",
  1791. },
  1792. {
  1793. description: "quota fails, though covering quota for configured limited scope PriorityClassNameExists exists.",
  1794. testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "20Gi"), getResourceList("", "")), "fake-priority"),
  1795. quota: &corev1.ResourceQuota{
  1796. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1797. Spec: corev1.ResourceQuotaSpec{
  1798. ScopeSelector: &corev1.ScopeSelector{
  1799. MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
  1800. {
  1801. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1802. Operator: corev1.ScopeSelectorOpExists},
  1803. },
  1804. },
  1805. },
  1806. Status: corev1.ResourceQuotaStatus{
  1807. Hard: corev1.ResourceList{
  1808. corev1.ResourceMemory: resource.MustParse("10Gi"),
  1809. },
  1810. Used: corev1.ResourceList{
  1811. corev1.ResourceMemory: resource.MustParse("1Gi"),
  1812. },
  1813. },
  1814. },
  1815. config: &resourcequotaapi.Configuration{
  1816. LimitedResources: []resourcequotaapi.LimitedResource{
  1817. {
  1818. Resource: "pods",
  1819. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1820. {
  1821. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1822. Operator: corev1.ScopeSelectorOpExists,
  1823. },
  1824. },
  1825. },
  1826. },
  1827. },
  1828. expErr: "forbidden: exceeded quota: quota, requested: memory=20Gi, used: memory=1Gi, limited: memory=10Gi",
  1829. },
  1830. {
  1831. description: "Pod has different priorityclass than configured limited. Covering quota exists for configured limited scope PriorityClassIn.",
  1832. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
  1833. quota: &corev1.ResourceQuota{
  1834. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1835. Spec: corev1.ResourceQuotaSpec{
  1836. ScopeSelector: &corev1.ScopeSelector{
  1837. MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
  1838. {
  1839. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1840. Operator: corev1.ScopeSelectorOpIn,
  1841. Values: []string{"cluster-services"},
  1842. },
  1843. },
  1844. },
  1845. },
  1846. },
  1847. config: &resourcequotaapi.Configuration{
  1848. LimitedResources: []resourcequotaapi.LimitedResource{
  1849. {
  1850. Resource: "pods",
  1851. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1852. {
  1853. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1854. Operator: corev1.ScopeSelectorOpIn,
  1855. Values: []string{"cluster-services"},
  1856. },
  1857. },
  1858. },
  1859. },
  1860. },
  1861. expErr: "",
  1862. },
  1863. {
  1864. description: "Pod has limited priorityclass. Covering quota exists for configured limited scope PriorityClassIn.",
  1865. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
  1866. quota: &corev1.ResourceQuota{
  1867. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1868. Spec: corev1.ResourceQuotaSpec{
  1869. ScopeSelector: &corev1.ScopeSelector{
  1870. MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
  1871. {
  1872. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1873. Operator: corev1.ScopeSelectorOpIn,
  1874. Values: []string{"cluster-services"},
  1875. },
  1876. },
  1877. },
  1878. },
  1879. },
  1880. config: &resourcequotaapi.Configuration{
  1881. LimitedResources: []resourcequotaapi.LimitedResource{
  1882. {
  1883. Resource: "pods",
  1884. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1885. {
  1886. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1887. Operator: corev1.ScopeSelectorOpIn,
  1888. Values: []string{"another-priorityclass-name", "cluster-services"},
  1889. },
  1890. },
  1891. },
  1892. },
  1893. },
  1894. expErr: "",
  1895. },
  1896. {
  1897. description: "Pod has limited priorityclass. Covering quota does not exist for configured limited scope PriorityClassIn.",
  1898. testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
  1899. quota: &corev1.ResourceQuota{
  1900. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1901. Spec: corev1.ResourceQuotaSpec{
  1902. ScopeSelector: &corev1.ScopeSelector{
  1903. MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
  1904. {
  1905. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1906. Operator: corev1.ScopeSelectorOpIn,
  1907. Values: []string{"another-priorityclass-name"},
  1908. },
  1909. },
  1910. },
  1911. },
  1912. },
  1913. config: &resourcequotaapi.Configuration{
  1914. LimitedResources: []resourcequotaapi.LimitedResource{
  1915. {
  1916. Resource: "pods",
  1917. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1918. {
  1919. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1920. Operator: corev1.ScopeSelectorOpIn,
  1921. Values: []string{"another-priorityclass-name", "cluster-services"},
  1922. },
  1923. },
  1924. },
  1925. },
  1926. },
  1927. expErr: "insufficient quota to match these scopes: [{PriorityClass In [another-priorityclass-name cluster-services]}]",
  1928. },
  1929. {
  1930. description: "From the above test case, just changing pod priority from cluster-services to another-priorityclass-name. expecting no error",
  1931. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "another-priorityclass-name"),
  1932. quota: &corev1.ResourceQuota{
  1933. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1934. Spec: corev1.ResourceQuotaSpec{
  1935. ScopeSelector: &corev1.ScopeSelector{
  1936. MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
  1937. {
  1938. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1939. Operator: corev1.ScopeSelectorOpIn,
  1940. Values: []string{"another-priorityclass-name"},
  1941. },
  1942. },
  1943. },
  1944. },
  1945. },
  1946. config: &resourcequotaapi.Configuration{
  1947. LimitedResources: []resourcequotaapi.LimitedResource{
  1948. {
  1949. Resource: "pods",
  1950. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1951. {
  1952. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1953. Operator: corev1.ScopeSelectorOpIn,
  1954. Values: []string{"another-priorityclass-name", "cluster-services"},
  1955. },
  1956. },
  1957. },
  1958. },
  1959. },
  1960. expErr: "",
  1961. },
  1962. {
  1963. description: "Pod has limited priorityclass. Covering quota does NOT exists for configured limited scope PriorityClassIn.",
  1964. testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
  1965. quota: &corev1.ResourceQuota{},
  1966. config: &resourcequotaapi.Configuration{
  1967. LimitedResources: []resourcequotaapi.LimitedResource{
  1968. {
  1969. Resource: "pods",
  1970. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  1971. {
  1972. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1973. Operator: corev1.ScopeSelectorOpIn,
  1974. Values: []string{"another-priorityclass-name", "cluster-services"},
  1975. },
  1976. },
  1977. },
  1978. },
  1979. },
  1980. expErr: "insufficient quota to match these scopes: [{PriorityClass In [another-priorityclass-name cluster-services]}]",
  1981. },
  1982. {
  1983. description: "Pod has limited priorityclass. Covering quota exists for configured limited scope PriorityClassIn through PriorityClassNameExists",
  1984. testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
  1985. quota: &corev1.ResourceQuota{
  1986. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
  1987. Spec: corev1.ResourceQuotaSpec{
  1988. ScopeSelector: &corev1.ScopeSelector{
  1989. MatchExpressions: []corev1.ScopedResourceSelectorRequirement{
  1990. {
  1991. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  1992. Operator: corev1.ScopeSelectorOpExists},
  1993. },
  1994. },
  1995. },
  1996. },
  1997. config: &resourcequotaapi.Configuration{
  1998. LimitedResources: []resourcequotaapi.LimitedResource{
  1999. {
  2000. Resource: "pods",
  2001. MatchScopes: []corev1.ScopedResourceSelectorRequirement{
  2002. {
  2003. ScopeName: corev1.ResourceQuotaScopePriorityClass,
  2004. Operator: corev1.ScopeSelectorOpIn,
  2005. Values: []string{"another-priorityclass-name", "cluster-services"},
  2006. },
  2007. },
  2008. },
  2009. },
  2010. },
  2011. expErr: "",
  2012. },
  2013. }
  2014. for _, testCase := range testCases {
  2015. newPod := testCase.testPod
  2016. config := testCase.config
  2017. resourceQuota := testCase.quota
  2018. kubeClient := fake.NewSimpleClientset(resourceQuota)
  2019. if testCase.anotherQuota != nil {
  2020. kubeClient = fake.NewSimpleClientset(resourceQuota, testCase.anotherQuota)
  2021. }
  2022. indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
  2023. stopCh := make(chan struct{})
  2024. defer close(stopCh)
  2025. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  2026. quotaAccessor, _ := newQuotaAccessor()
  2027. quotaAccessor.client = kubeClient
  2028. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  2029. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  2030. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  2031. handler := &QuotaAdmission{
  2032. Handler: admission.NewHandler(admission.Create, admission.Update),
  2033. evaluator: evaluator,
  2034. }
  2035. indexer.Add(resourceQuota)
  2036. if testCase.anotherQuota != nil {
  2037. indexer.Add(testCase.anotherQuota)
  2038. }
  2039. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, corev1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  2040. if testCase.expErr == "" {
  2041. if err != nil {
  2042. t.Fatalf("Testcase, %v, failed with unexpected error: %v. ExpErr: %v", testCase.description, err, testCase.expErr)
  2043. }
  2044. } else {
  2045. if !strings.Contains(fmt.Sprintf("%v", err), testCase.expErr) {
  2046. t.Fatalf("Testcase, %v, failed with unexpected error: %v. ExpErr: %v", testCase.description, err, testCase.expErr)
  2047. }
  2048. }
  2049. }
  2050. }
  2051. // TestAdmitZeroDeltaUsageWithoutCoveringQuota verifies that resource quota is not required for zero delta requests.
  2052. func TestAdmitZeroDeltaUsageWithoutCoveringQuota(t *testing.T) {
  2053. kubeClient := fake.NewSimpleClientset()
  2054. stopCh := make(chan struct{})
  2055. defer close(stopCh)
  2056. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  2057. quotaAccessor, _ := newQuotaAccessor()
  2058. quotaAccessor.client = kubeClient
  2059. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  2060. // disable services unless there is a covering quota.
  2061. config := &resourcequotaapi.Configuration{
  2062. LimitedResources: []resourcequotaapi.LimitedResource{
  2063. {
  2064. Resource: "services",
  2065. MatchContains: []string{"services.loadbalancers"},
  2066. },
  2067. },
  2068. }
  2069. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  2070. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  2071. handler := &QuotaAdmission{
  2072. Handler: admission.NewHandler(admission.Create, admission.Update),
  2073. evaluator: evaluator,
  2074. }
  2075. existingService := &api.Service{
  2076. ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test", ResourceVersion: "1"},
  2077. Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
  2078. }
  2079. newService := &api.Service{
  2080. ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test"},
  2081. Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
  2082. }
  2083. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newService, existingService, api.Kind("Service").WithVersion("version"), newService.Namespace, newService.Name, corev1.Resource("services").WithVersion("version"), "", admission.Update, &metav1.CreateOptions{}, false, nil), nil)
  2084. if err != nil {
  2085. t.Errorf("unexpected error: %v", err)
  2086. }
  2087. }
  2088. // TestAdmitRejectIncreaseUsageWithoutCoveringQuota verifies that resource quota is required for delta requests that increase usage.
  2089. func TestAdmitRejectIncreaseUsageWithoutCoveringQuota(t *testing.T) {
  2090. kubeClient := fake.NewSimpleClientset()
  2091. stopCh := make(chan struct{})
  2092. defer close(stopCh)
  2093. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  2094. quotaAccessor, _ := newQuotaAccessor()
  2095. quotaAccessor.client = kubeClient
  2096. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  2097. // disable services unless there is a covering quota.
  2098. config := &resourcequotaapi.Configuration{
  2099. LimitedResources: []resourcequotaapi.LimitedResource{
  2100. {
  2101. Resource: "services",
  2102. MatchContains: []string{"services.loadbalancers"},
  2103. },
  2104. },
  2105. }
  2106. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  2107. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  2108. handler := &QuotaAdmission{
  2109. Handler: admission.NewHandler(admission.Create, admission.Update),
  2110. evaluator: evaluator,
  2111. }
  2112. existingService := &api.Service{
  2113. ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test", ResourceVersion: "1"},
  2114. Spec: api.ServiceSpec{
  2115. Type: api.ServiceTypeNodePort,
  2116. Ports: []api.ServicePort{{Port: 1234}},
  2117. },
  2118. }
  2119. newService := &api.Service{
  2120. ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test"},
  2121. Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
  2122. }
  2123. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newService, existingService, api.Kind("Service").WithVersion("version"), newService.Namespace, newService.Name, corev1.Resource("services").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
  2124. if err == nil {
  2125. t.Errorf("Expected an error for consuming a limited resource without quota.")
  2126. }
  2127. }
  2128. // TestAdmitAllowDecreaseUsageWithoutCoveringQuota verifies that resource quota is not required for delta requests that decrease usage.
  2129. func TestAdmitAllowDecreaseUsageWithoutCoveringQuota(t *testing.T) {
  2130. kubeClient := fake.NewSimpleClientset()
  2131. stopCh := make(chan struct{})
  2132. defer close(stopCh)
  2133. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  2134. quotaAccessor, _ := newQuotaAccessor()
  2135. quotaAccessor.client = kubeClient
  2136. quotaAccessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
  2137. // disable services unless there is a covering quota.
  2138. config := &resourcequotaapi.Configuration{
  2139. LimitedResources: []resourcequotaapi.LimitedResource{
  2140. {
  2141. Resource: "services",
  2142. MatchContains: []string{"services.loadbalancers"},
  2143. },
  2144. },
  2145. }
  2146. quotaConfiguration := install.NewQuotaConfigurationForAdmission()
  2147. evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
  2148. handler := &QuotaAdmission{
  2149. Handler: admission.NewHandler(admission.Create, admission.Update),
  2150. evaluator: evaluator,
  2151. }
  2152. existingService := &api.Service{
  2153. ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test", ResourceVersion: "1"},
  2154. Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer},
  2155. }
  2156. newService := &api.Service{
  2157. ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test"},
  2158. Spec: api.ServiceSpec{
  2159. Type: api.ServiceTypeNodePort,
  2160. Ports: []api.ServicePort{{Port: 1234}},
  2161. },
  2162. }
  2163. err := handler.Validate(context.TODO(), admission.NewAttributesRecord(newService, existingService, api.Kind("Service").WithVersion("version"), newService.Namespace, newService.Name, corev1.Resource("services").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
  2164. if err != nil {
  2165. t.Errorf("Expected no error for decreasing a limited resource without quota, got %v", err)
  2166. }
  2167. }