resource_quota_controller_test.go 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253
  1. /*
  2. Copyright 2015 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. "fmt"
  16. "net/http"
  17. "net/http/httptest"
  18. "strings"
  19. "sync"
  20. "testing"
  21. "time"
  22. "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/labels"
  26. "k8s.io/apimachinery/pkg/runtime"
  27. "k8s.io/apimachinery/pkg/runtime/schema"
  28. "k8s.io/apimachinery/pkg/util/sets"
  29. "k8s.io/client-go/informers"
  30. "k8s.io/client-go/kubernetes"
  31. "k8s.io/client-go/kubernetes/fake"
  32. "k8s.io/client-go/rest"
  33. core "k8s.io/client-go/testing"
  34. "k8s.io/client-go/tools/cache"
  35. "k8s.io/kubernetes/pkg/controller"
  36. quota "k8s.io/kubernetes/pkg/quota/v1"
  37. "k8s.io/kubernetes/pkg/quota/v1/generic"
  38. "k8s.io/kubernetes/pkg/quota/v1/install"
  39. )
  40. func getResourceList(cpu, memory string) v1.ResourceList {
  41. res := v1.ResourceList{}
  42. if cpu != "" {
  43. res[v1.ResourceCPU] = resource.MustParse(cpu)
  44. }
  45. if memory != "" {
  46. res[v1.ResourceMemory] = resource.MustParse(memory)
  47. }
  48. return res
  49. }
  50. func getResourceRequirements(requests, limits v1.ResourceList) v1.ResourceRequirements {
  51. res := v1.ResourceRequirements{}
  52. res.Requests = requests
  53. res.Limits = limits
  54. return res
  55. }
  56. func mockDiscoveryFunc() ([]*metav1.APIResourceList, error) {
  57. return []*metav1.APIResourceList{}, nil
  58. }
  59. func mockListerForResourceFunc(listersForResource map[schema.GroupVersionResource]cache.GenericLister) quota.ListerForResourceFunc {
  60. return func(gvr schema.GroupVersionResource) (cache.GenericLister, error) {
  61. lister, found := listersForResource[gvr]
  62. if !found {
  63. return nil, fmt.Errorf("no lister found for resource")
  64. }
  65. return lister, nil
  66. }
  67. }
  68. func newGenericLister(groupResource schema.GroupResource, items []runtime.Object) cache.GenericLister {
  69. store := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
  70. for _, item := range items {
  71. store.Add(item)
  72. }
  73. return cache.NewGenericLister(store, groupResource)
  74. }
  75. func newErrorLister() cache.GenericLister {
  76. return errorLister{}
  77. }
  78. type errorLister struct {
  79. }
  80. func (errorLister) List(selector labels.Selector) (ret []runtime.Object, err error) {
  81. return nil, fmt.Errorf("error listing")
  82. }
  83. func (errorLister) Get(name string) (runtime.Object, error) {
  84. return nil, fmt.Errorf("error getting")
  85. }
  86. func (errorLister) ByNamespace(namespace string) cache.GenericNamespaceLister {
  87. return errorLister{}
  88. }
  89. type quotaController struct {
  90. *ResourceQuotaController
  91. stop chan struct{}
  92. }
  93. func setupQuotaController(t *testing.T, kubeClient kubernetes.Interface, lister quota.ListerForResourceFunc, discoveryFunc NamespacedResourcesFunc) quotaController {
  94. informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
  95. quotaConfiguration := install.NewQuotaConfigurationForControllers(lister)
  96. alwaysStarted := make(chan struct{})
  97. close(alwaysStarted)
  98. resourceQuotaControllerOptions := &ResourceQuotaControllerOptions{
  99. QuotaClient: kubeClient.CoreV1(),
  100. ResourceQuotaInformer: informerFactory.Core().V1().ResourceQuotas(),
  101. ResyncPeriod: controller.NoResyncPeriodFunc,
  102. ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
  103. IgnoredResourcesFunc: quotaConfiguration.IgnoredResources,
  104. DiscoveryFunc: discoveryFunc,
  105. Registry: generic.NewRegistry(quotaConfiguration.Evaluators()),
  106. InformersStarted: alwaysStarted,
  107. InformerFactory: informerFactory,
  108. }
  109. qc, err := NewResourceQuotaController(resourceQuotaControllerOptions)
  110. if err != nil {
  111. t.Fatal(err)
  112. }
  113. stop := make(chan struct{})
  114. informerFactory.Start(stop)
  115. return quotaController{qc, stop}
  116. }
  117. func newTestPods() []runtime.Object {
  118. return []runtime.Object{
  119. &v1.Pod{
  120. ObjectMeta: metav1.ObjectMeta{Name: "pod-running", Namespace: "testing"},
  121. Status: v1.PodStatus{Phase: v1.PodRunning},
  122. Spec: v1.PodSpec{
  123. Volumes: []v1.Volume{{Name: "vol"}},
  124. Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
  125. },
  126. },
  127. &v1.Pod{
  128. ObjectMeta: metav1.ObjectMeta{Name: "pod-running-2", Namespace: "testing"},
  129. Status: v1.PodStatus{Phase: v1.PodRunning},
  130. Spec: v1.PodSpec{
  131. Volumes: []v1.Volume{{Name: "vol"}},
  132. Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
  133. },
  134. },
  135. &v1.Pod{
  136. ObjectMeta: metav1.ObjectMeta{Name: "pod-failed", Namespace: "testing"},
  137. Status: v1.PodStatus{Phase: v1.PodFailed},
  138. Spec: v1.PodSpec{
  139. Volumes: []v1.Volume{{Name: "vol"}},
  140. Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
  141. },
  142. },
  143. }
  144. }
  145. func newBestEffortTestPods() []runtime.Object {
  146. return []runtime.Object{
  147. &v1.Pod{
  148. ObjectMeta: metav1.ObjectMeta{Name: "pod-running", Namespace: "testing"},
  149. Status: v1.PodStatus{Phase: v1.PodRunning},
  150. Spec: v1.PodSpec{
  151. Volumes: []v1.Volume{{Name: "vol"}},
  152. Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("", ""), getResourceList("", ""))}},
  153. },
  154. },
  155. &v1.Pod{
  156. ObjectMeta: metav1.ObjectMeta{Name: "pod-running-2", Namespace: "testing"},
  157. Status: v1.PodStatus{Phase: v1.PodRunning},
  158. Spec: v1.PodSpec{
  159. Volumes: []v1.Volume{{Name: "vol"}},
  160. Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("", ""), getResourceList("", ""))}},
  161. },
  162. },
  163. &v1.Pod{
  164. ObjectMeta: metav1.ObjectMeta{Name: "pod-failed", Namespace: "testing"},
  165. Status: v1.PodStatus{Phase: v1.PodFailed},
  166. Spec: v1.PodSpec{
  167. Volumes: []v1.Volume{{Name: "vol"}},
  168. Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
  169. },
  170. },
  171. }
  172. }
  173. func newTestPodsWithPriorityClasses() []runtime.Object {
  174. return []runtime.Object{
  175. &v1.Pod{
  176. ObjectMeta: metav1.ObjectMeta{Name: "pod-running", Namespace: "testing"},
  177. Status: v1.PodStatus{Phase: v1.PodRunning},
  178. Spec: v1.PodSpec{
  179. Volumes: []v1.Volume{{Name: "vol"}},
  180. Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("500m", "50Gi"), getResourceList("", ""))}},
  181. PriorityClassName: "high",
  182. },
  183. },
  184. &v1.Pod{
  185. ObjectMeta: metav1.ObjectMeta{Name: "pod-running-2", Namespace: "testing"},
  186. Status: v1.PodStatus{Phase: v1.PodRunning},
  187. Spec: v1.PodSpec{
  188. Volumes: []v1.Volume{{Name: "vol"}},
  189. Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
  190. PriorityClassName: "low",
  191. },
  192. },
  193. &v1.Pod{
  194. ObjectMeta: metav1.ObjectMeta{Name: "pod-failed", Namespace: "testing"},
  195. Status: v1.PodStatus{Phase: v1.PodFailed},
  196. Spec: v1.PodSpec{
  197. Volumes: []v1.Volume{{Name: "vol"}},
  198. Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}},
  199. },
  200. },
  201. }
  202. }
  203. func TestSyncResourceQuota(t *testing.T) {
  204. testCases := map[string]struct {
  205. gvr schema.GroupVersionResource
  206. errorGVR schema.GroupVersionResource
  207. items []runtime.Object
  208. quota v1.ResourceQuota
  209. status v1.ResourceQuotaStatus
  210. expectedError string
  211. expectedActionSet sets.String
  212. }{
  213. "non-matching-best-effort-scoped-quota": {
  214. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  215. quota: v1.ResourceQuota{
  216. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  217. Spec: v1.ResourceQuotaSpec{
  218. Hard: v1.ResourceList{
  219. v1.ResourceCPU: resource.MustParse("3"),
  220. v1.ResourceMemory: resource.MustParse("100Gi"),
  221. v1.ResourcePods: resource.MustParse("5"),
  222. },
  223. Scopes: []v1.ResourceQuotaScope{v1.ResourceQuotaScopeBestEffort},
  224. },
  225. },
  226. status: v1.ResourceQuotaStatus{
  227. Hard: v1.ResourceList{
  228. v1.ResourceCPU: resource.MustParse("3"),
  229. v1.ResourceMemory: resource.MustParse("100Gi"),
  230. v1.ResourcePods: resource.MustParse("5"),
  231. },
  232. Used: v1.ResourceList{
  233. v1.ResourceCPU: resource.MustParse("0"),
  234. v1.ResourceMemory: resource.MustParse("0"),
  235. v1.ResourcePods: resource.MustParse("0"),
  236. },
  237. },
  238. expectedActionSet: sets.NewString(
  239. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  240. ),
  241. items: newTestPods(),
  242. },
  243. "matching-best-effort-scoped-quota": {
  244. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  245. quota: v1.ResourceQuota{
  246. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  247. Spec: v1.ResourceQuotaSpec{
  248. Hard: v1.ResourceList{
  249. v1.ResourceCPU: resource.MustParse("3"),
  250. v1.ResourceMemory: resource.MustParse("100Gi"),
  251. v1.ResourcePods: resource.MustParse("5"),
  252. },
  253. Scopes: []v1.ResourceQuotaScope{v1.ResourceQuotaScopeBestEffort},
  254. },
  255. },
  256. status: v1.ResourceQuotaStatus{
  257. Hard: v1.ResourceList{
  258. v1.ResourceCPU: resource.MustParse("3"),
  259. v1.ResourceMemory: resource.MustParse("100Gi"),
  260. v1.ResourcePods: resource.MustParse("5"),
  261. },
  262. Used: v1.ResourceList{
  263. v1.ResourceCPU: resource.MustParse("0"),
  264. v1.ResourceMemory: resource.MustParse("0"),
  265. v1.ResourcePods: resource.MustParse("2"),
  266. },
  267. },
  268. expectedActionSet: sets.NewString(
  269. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  270. ),
  271. items: newBestEffortTestPods(),
  272. },
  273. "non-matching-priorityclass-scoped-quota-OpExists": {
  274. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  275. quota: v1.ResourceQuota{
  276. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  277. Spec: v1.ResourceQuotaSpec{
  278. Hard: v1.ResourceList{
  279. v1.ResourceCPU: resource.MustParse("3"),
  280. v1.ResourceMemory: resource.MustParse("100Gi"),
  281. v1.ResourcePods: resource.MustParse("5"),
  282. },
  283. ScopeSelector: &v1.ScopeSelector{
  284. MatchExpressions: []v1.ScopedResourceSelectorRequirement{
  285. {
  286. ScopeName: v1.ResourceQuotaScopePriorityClass,
  287. Operator: v1.ScopeSelectorOpExists},
  288. },
  289. },
  290. },
  291. },
  292. status: v1.ResourceQuotaStatus{
  293. Hard: v1.ResourceList{
  294. v1.ResourceCPU: resource.MustParse("3"),
  295. v1.ResourceMemory: resource.MustParse("100Gi"),
  296. v1.ResourcePods: resource.MustParse("5"),
  297. },
  298. Used: v1.ResourceList{
  299. v1.ResourceCPU: resource.MustParse("0"),
  300. v1.ResourceMemory: resource.MustParse("0"),
  301. v1.ResourcePods: resource.MustParse("0"),
  302. },
  303. },
  304. expectedActionSet: sets.NewString(
  305. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  306. ),
  307. items: newTestPods(),
  308. },
  309. "matching-priorityclass-scoped-quota-OpExists": {
  310. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  311. quota: v1.ResourceQuota{
  312. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  313. Spec: v1.ResourceQuotaSpec{
  314. Hard: v1.ResourceList{
  315. v1.ResourceCPU: resource.MustParse("3"),
  316. v1.ResourceMemory: resource.MustParse("100Gi"),
  317. v1.ResourcePods: resource.MustParse("5"),
  318. },
  319. ScopeSelector: &v1.ScopeSelector{
  320. MatchExpressions: []v1.ScopedResourceSelectorRequirement{
  321. {
  322. ScopeName: v1.ResourceQuotaScopePriorityClass,
  323. Operator: v1.ScopeSelectorOpExists},
  324. },
  325. },
  326. },
  327. },
  328. status: v1.ResourceQuotaStatus{
  329. Hard: v1.ResourceList{
  330. v1.ResourceCPU: resource.MustParse("3"),
  331. v1.ResourceMemory: resource.MustParse("100Gi"),
  332. v1.ResourcePods: resource.MustParse("5"),
  333. },
  334. Used: v1.ResourceList{
  335. v1.ResourceCPU: resource.MustParse("600m"),
  336. v1.ResourceMemory: resource.MustParse("51Gi"),
  337. v1.ResourcePods: resource.MustParse("2"),
  338. },
  339. },
  340. expectedActionSet: sets.NewString(
  341. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  342. ),
  343. items: newTestPodsWithPriorityClasses(),
  344. },
  345. "matching-priorityclass-scoped-quota-OpIn": {
  346. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  347. quota: v1.ResourceQuota{
  348. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  349. Spec: v1.ResourceQuotaSpec{
  350. Hard: v1.ResourceList{
  351. v1.ResourceCPU: resource.MustParse("3"),
  352. v1.ResourceMemory: resource.MustParse("100Gi"),
  353. v1.ResourcePods: resource.MustParse("5"),
  354. },
  355. ScopeSelector: &v1.ScopeSelector{
  356. MatchExpressions: []v1.ScopedResourceSelectorRequirement{
  357. {
  358. ScopeName: v1.ResourceQuotaScopePriorityClass,
  359. Operator: v1.ScopeSelectorOpIn,
  360. Values: []string{"high", "low"},
  361. },
  362. },
  363. },
  364. },
  365. },
  366. status: v1.ResourceQuotaStatus{
  367. Hard: v1.ResourceList{
  368. v1.ResourceCPU: resource.MustParse("3"),
  369. v1.ResourceMemory: resource.MustParse("100Gi"),
  370. v1.ResourcePods: resource.MustParse("5"),
  371. },
  372. Used: v1.ResourceList{
  373. v1.ResourceCPU: resource.MustParse("600m"),
  374. v1.ResourceMemory: resource.MustParse("51Gi"),
  375. v1.ResourcePods: resource.MustParse("2"),
  376. },
  377. },
  378. expectedActionSet: sets.NewString(
  379. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  380. ),
  381. items: newTestPodsWithPriorityClasses(),
  382. },
  383. "matching-priorityclass-scoped-quota-OpIn-high": {
  384. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  385. quota: v1.ResourceQuota{
  386. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  387. Spec: v1.ResourceQuotaSpec{
  388. Hard: v1.ResourceList{
  389. v1.ResourceCPU: resource.MustParse("3"),
  390. v1.ResourceMemory: resource.MustParse("100Gi"),
  391. v1.ResourcePods: resource.MustParse("5"),
  392. },
  393. ScopeSelector: &v1.ScopeSelector{
  394. MatchExpressions: []v1.ScopedResourceSelectorRequirement{
  395. {
  396. ScopeName: v1.ResourceQuotaScopePriorityClass,
  397. Operator: v1.ScopeSelectorOpIn,
  398. Values: []string{"high"},
  399. },
  400. },
  401. },
  402. },
  403. },
  404. status: v1.ResourceQuotaStatus{
  405. Hard: v1.ResourceList{
  406. v1.ResourceCPU: resource.MustParse("3"),
  407. v1.ResourceMemory: resource.MustParse("100Gi"),
  408. v1.ResourcePods: resource.MustParse("5"),
  409. },
  410. Used: v1.ResourceList{
  411. v1.ResourceCPU: resource.MustParse("500m"),
  412. v1.ResourceMemory: resource.MustParse("50Gi"),
  413. v1.ResourcePods: resource.MustParse("1"),
  414. },
  415. },
  416. expectedActionSet: sets.NewString(
  417. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  418. ),
  419. items: newTestPodsWithPriorityClasses(),
  420. },
  421. "matching-priorityclass-scoped-quota-OpIn-low": {
  422. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  423. quota: v1.ResourceQuota{
  424. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  425. Spec: v1.ResourceQuotaSpec{
  426. Hard: v1.ResourceList{
  427. v1.ResourceCPU: resource.MustParse("3"),
  428. v1.ResourceMemory: resource.MustParse("100Gi"),
  429. v1.ResourcePods: resource.MustParse("5"),
  430. },
  431. ScopeSelector: &v1.ScopeSelector{
  432. MatchExpressions: []v1.ScopedResourceSelectorRequirement{
  433. {
  434. ScopeName: v1.ResourceQuotaScopePriorityClass,
  435. Operator: v1.ScopeSelectorOpIn,
  436. Values: []string{"low"},
  437. },
  438. },
  439. },
  440. },
  441. },
  442. status: v1.ResourceQuotaStatus{
  443. Hard: v1.ResourceList{
  444. v1.ResourceCPU: resource.MustParse("3"),
  445. v1.ResourceMemory: resource.MustParse("100Gi"),
  446. v1.ResourcePods: resource.MustParse("5"),
  447. },
  448. Used: v1.ResourceList{
  449. v1.ResourceCPU: resource.MustParse("100m"),
  450. v1.ResourceMemory: resource.MustParse("1Gi"),
  451. v1.ResourcePods: resource.MustParse("1"),
  452. },
  453. },
  454. expectedActionSet: sets.NewString(
  455. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  456. ),
  457. items: newTestPodsWithPriorityClasses(),
  458. },
  459. "matching-priorityclass-scoped-quota-OpNotIn-low": {
  460. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  461. quota: v1.ResourceQuota{
  462. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  463. Spec: v1.ResourceQuotaSpec{
  464. Hard: v1.ResourceList{
  465. v1.ResourceCPU: resource.MustParse("3"),
  466. v1.ResourceMemory: resource.MustParse("100Gi"),
  467. v1.ResourcePods: resource.MustParse("5"),
  468. },
  469. ScopeSelector: &v1.ScopeSelector{
  470. MatchExpressions: []v1.ScopedResourceSelectorRequirement{
  471. {
  472. ScopeName: v1.ResourceQuotaScopePriorityClass,
  473. Operator: v1.ScopeSelectorOpNotIn,
  474. Values: []string{"high"},
  475. },
  476. },
  477. },
  478. },
  479. },
  480. status: v1.ResourceQuotaStatus{
  481. Hard: v1.ResourceList{
  482. v1.ResourceCPU: resource.MustParse("3"),
  483. v1.ResourceMemory: resource.MustParse("100Gi"),
  484. v1.ResourcePods: resource.MustParse("5"),
  485. },
  486. Used: v1.ResourceList{
  487. v1.ResourceCPU: resource.MustParse("100m"),
  488. v1.ResourceMemory: resource.MustParse("1Gi"),
  489. v1.ResourcePods: resource.MustParse("1"),
  490. },
  491. },
  492. expectedActionSet: sets.NewString(
  493. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  494. ),
  495. items: newTestPodsWithPriorityClasses(),
  496. },
  497. "non-matching-priorityclass-scoped-quota-OpIn": {
  498. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  499. quota: v1.ResourceQuota{
  500. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  501. Spec: v1.ResourceQuotaSpec{
  502. Hard: v1.ResourceList{
  503. v1.ResourceCPU: resource.MustParse("3"),
  504. v1.ResourceMemory: resource.MustParse("100Gi"),
  505. v1.ResourcePods: resource.MustParse("5"),
  506. },
  507. ScopeSelector: &v1.ScopeSelector{
  508. MatchExpressions: []v1.ScopedResourceSelectorRequirement{
  509. {
  510. ScopeName: v1.ResourceQuotaScopePriorityClass,
  511. Operator: v1.ScopeSelectorOpIn,
  512. Values: []string{"random"},
  513. },
  514. },
  515. },
  516. },
  517. },
  518. status: v1.ResourceQuotaStatus{
  519. Hard: v1.ResourceList{
  520. v1.ResourceCPU: resource.MustParse("3"),
  521. v1.ResourceMemory: resource.MustParse("100Gi"),
  522. v1.ResourcePods: resource.MustParse("5"),
  523. },
  524. Used: v1.ResourceList{
  525. v1.ResourceCPU: resource.MustParse("0"),
  526. v1.ResourceMemory: resource.MustParse("0"),
  527. v1.ResourcePods: resource.MustParse("0"),
  528. },
  529. },
  530. expectedActionSet: sets.NewString(
  531. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  532. ),
  533. items: newTestPodsWithPriorityClasses(),
  534. },
  535. "non-matching-priorityclass-scoped-quota-OpNotIn": {
  536. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  537. quota: v1.ResourceQuota{
  538. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  539. Spec: v1.ResourceQuotaSpec{
  540. Hard: v1.ResourceList{
  541. v1.ResourceCPU: resource.MustParse("3"),
  542. v1.ResourceMemory: resource.MustParse("100Gi"),
  543. v1.ResourcePods: resource.MustParse("5"),
  544. },
  545. ScopeSelector: &v1.ScopeSelector{
  546. MatchExpressions: []v1.ScopedResourceSelectorRequirement{
  547. {
  548. ScopeName: v1.ResourceQuotaScopePriorityClass,
  549. Operator: v1.ScopeSelectorOpNotIn,
  550. Values: []string{"random"},
  551. },
  552. },
  553. },
  554. },
  555. },
  556. status: v1.ResourceQuotaStatus{
  557. Hard: v1.ResourceList{
  558. v1.ResourceCPU: resource.MustParse("3"),
  559. v1.ResourceMemory: resource.MustParse("100Gi"),
  560. v1.ResourcePods: resource.MustParse("5"),
  561. },
  562. Used: v1.ResourceList{
  563. v1.ResourceCPU: resource.MustParse("200m"),
  564. v1.ResourceMemory: resource.MustParse("2Gi"),
  565. v1.ResourcePods: resource.MustParse("2"),
  566. },
  567. },
  568. expectedActionSet: sets.NewString(
  569. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  570. ),
  571. items: newTestPods(),
  572. },
  573. "matching-priorityclass-scoped-quota-OpDoesNotExist": {
  574. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  575. quota: v1.ResourceQuota{
  576. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  577. Spec: v1.ResourceQuotaSpec{
  578. Hard: v1.ResourceList{
  579. v1.ResourceCPU: resource.MustParse("3"),
  580. v1.ResourceMemory: resource.MustParse("100Gi"),
  581. v1.ResourcePods: resource.MustParse("5"),
  582. },
  583. ScopeSelector: &v1.ScopeSelector{
  584. MatchExpressions: []v1.ScopedResourceSelectorRequirement{
  585. {
  586. ScopeName: v1.ResourceQuotaScopePriorityClass,
  587. Operator: v1.ScopeSelectorOpDoesNotExist,
  588. },
  589. },
  590. },
  591. },
  592. },
  593. status: v1.ResourceQuotaStatus{
  594. Hard: v1.ResourceList{
  595. v1.ResourceCPU: resource.MustParse("3"),
  596. v1.ResourceMemory: resource.MustParse("100Gi"),
  597. v1.ResourcePods: resource.MustParse("5"),
  598. },
  599. Used: v1.ResourceList{
  600. v1.ResourceCPU: resource.MustParse("200m"),
  601. v1.ResourceMemory: resource.MustParse("2Gi"),
  602. v1.ResourcePods: resource.MustParse("2"),
  603. },
  604. },
  605. expectedActionSet: sets.NewString(
  606. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  607. ),
  608. items: newTestPods(),
  609. },
  610. "pods": {
  611. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  612. quota: v1.ResourceQuota{
  613. ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"},
  614. Spec: v1.ResourceQuotaSpec{
  615. Hard: v1.ResourceList{
  616. v1.ResourceCPU: resource.MustParse("3"),
  617. v1.ResourceMemory: resource.MustParse("100Gi"),
  618. v1.ResourcePods: resource.MustParse("5"),
  619. },
  620. },
  621. },
  622. status: v1.ResourceQuotaStatus{
  623. Hard: v1.ResourceList{
  624. v1.ResourceCPU: resource.MustParse("3"),
  625. v1.ResourceMemory: resource.MustParse("100Gi"),
  626. v1.ResourcePods: resource.MustParse("5"),
  627. },
  628. Used: v1.ResourceList{
  629. v1.ResourceCPU: resource.MustParse("200m"),
  630. v1.ResourceMemory: resource.MustParse("2Gi"),
  631. v1.ResourcePods: resource.MustParse("2"),
  632. },
  633. },
  634. expectedActionSet: sets.NewString(
  635. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  636. ),
  637. items: newTestPods(),
  638. },
  639. "quota-spec-hard-updated": {
  640. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  641. quota: v1.ResourceQuota{
  642. ObjectMeta: metav1.ObjectMeta{
  643. Namespace: "default",
  644. Name: "rq",
  645. },
  646. Spec: v1.ResourceQuotaSpec{
  647. Hard: v1.ResourceList{
  648. v1.ResourceCPU: resource.MustParse("4"),
  649. },
  650. },
  651. Status: v1.ResourceQuotaStatus{
  652. Hard: v1.ResourceList{
  653. v1.ResourceCPU: resource.MustParse("3"),
  654. },
  655. Used: v1.ResourceList{
  656. v1.ResourceCPU: resource.MustParse("0"),
  657. },
  658. },
  659. },
  660. status: v1.ResourceQuotaStatus{
  661. Hard: v1.ResourceList{
  662. v1.ResourceCPU: resource.MustParse("4"),
  663. },
  664. Used: v1.ResourceList{
  665. v1.ResourceCPU: resource.MustParse("0"),
  666. },
  667. },
  668. expectedActionSet: sets.NewString(
  669. strings.Join([]string{"update", "resourcequotas", "status"}, "-"),
  670. ),
  671. items: []runtime.Object{},
  672. },
  673. "quota-unchanged": {
  674. gvr: v1.SchemeGroupVersion.WithResource("pods"),
  675. quota: v1.ResourceQuota{
  676. ObjectMeta: metav1.ObjectMeta{
  677. Namespace: "default",
  678. Name: "rq",
  679. },
  680. Spec: v1.ResourceQuotaSpec{
  681. Hard: v1.ResourceList{
  682. v1.ResourceCPU: resource.MustParse("4"),
  683. },
  684. },
  685. Status: v1.ResourceQuotaStatus{
  686. Hard: v1.ResourceList{
  687. v1.ResourceCPU: resource.MustParse("0"),
  688. },
  689. },
  690. },
  691. status: v1.ResourceQuotaStatus{
  692. Hard: v1.ResourceList{
  693. v1.ResourceCPU: resource.MustParse("4"),
  694. },
  695. Used: v1.ResourceList{
  696. v1.ResourceCPU: resource.MustParse("0"),
  697. },
  698. },
  699. expectedActionSet: sets.NewString(),
  700. items: []runtime.Object{},
  701. },
  702. "quota-missing-status-with-calculation-error": {
  703. errorGVR: v1.SchemeGroupVersion.WithResource("pods"),
  704. quota: v1.ResourceQuota{
  705. ObjectMeta: metav1.ObjectMeta{
  706. Namespace: "default",
  707. Name: "rq",
  708. },
  709. Spec: v1.ResourceQuotaSpec{
  710. Hard: v1.ResourceList{
  711. v1.ResourcePods: resource.MustParse("1"),
  712. },
  713. },
  714. Status: v1.ResourceQuotaStatus{},
  715. },
  716. status: v1.ResourceQuotaStatus{
  717. Hard: v1.ResourceList{
  718. v1.ResourcePods: resource.MustParse("1"),
  719. },
  720. },
  721. expectedError: "error listing",
  722. expectedActionSet: sets.NewString("update-resourcequotas-status"),
  723. items: []runtime.Object{},
  724. },
  725. "quota-missing-status-with-partial-calculation-error": {
  726. gvr: v1.SchemeGroupVersion.WithResource("configmaps"),
  727. errorGVR: v1.SchemeGroupVersion.WithResource("pods"),
  728. quota: v1.ResourceQuota{
  729. ObjectMeta: metav1.ObjectMeta{
  730. Namespace: "default",
  731. Name: "rq",
  732. },
  733. Spec: v1.ResourceQuotaSpec{
  734. Hard: v1.ResourceList{
  735. v1.ResourcePods: resource.MustParse("1"),
  736. v1.ResourceConfigMaps: resource.MustParse("1"),
  737. },
  738. },
  739. Status: v1.ResourceQuotaStatus{},
  740. },
  741. status: v1.ResourceQuotaStatus{
  742. Hard: v1.ResourceList{
  743. v1.ResourcePods: resource.MustParse("1"),
  744. v1.ResourceConfigMaps: resource.MustParse("1"),
  745. },
  746. Used: v1.ResourceList{
  747. v1.ResourceConfigMaps: resource.MustParse("0"),
  748. },
  749. },
  750. expectedError: "error listing",
  751. expectedActionSet: sets.NewString("update-resourcequotas-status"),
  752. items: []runtime.Object{},
  753. },
  754. }
  755. for testName, testCase := range testCases {
  756. kubeClient := fake.NewSimpleClientset(&testCase.quota)
  757. listersForResourceConfig := map[schema.GroupVersionResource]cache.GenericLister{
  758. testCase.gvr: newGenericLister(testCase.gvr.GroupResource(), testCase.items),
  759. testCase.errorGVR: newErrorLister(),
  760. }
  761. qc := setupQuotaController(t, kubeClient, mockListerForResourceFunc(listersForResourceConfig), mockDiscoveryFunc)
  762. defer close(qc.stop)
  763. if err := qc.syncResourceQuota(&testCase.quota); err != nil {
  764. if len(testCase.expectedError) == 0 || !strings.Contains(err.Error(), testCase.expectedError) {
  765. t.Fatalf("test: %s, unexpected error: %v", testName, err)
  766. }
  767. } else if len(testCase.expectedError) > 0 {
  768. t.Fatalf("test: %s, expected error %q, got none", testName, testCase.expectedError)
  769. }
  770. actionSet := sets.NewString()
  771. for _, action := range kubeClient.Actions() {
  772. actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-"))
  773. }
  774. if !actionSet.HasAll(testCase.expectedActionSet.List()...) {
  775. t.Errorf("test: %s,\nExpected actions:\n%v\n but got:\n%v\nDifference:\n%v", testName, testCase.expectedActionSet, actionSet, testCase.expectedActionSet.Difference(actionSet))
  776. }
  777. var usage *v1.ResourceQuota
  778. actions := kubeClient.Actions()
  779. for i := len(actions) - 1; i >= 0; i-- {
  780. if updateAction, ok := actions[i].(core.UpdateAction); ok {
  781. usage = updateAction.GetObject().(*v1.ResourceQuota)
  782. break
  783. }
  784. }
  785. if usage == nil {
  786. t.Errorf("test: %s,\nExpected update action usage, got none: actions:\n%v", testName, actions)
  787. }
  788. // ensure usage is as expected
  789. if len(usage.Status.Hard) != len(testCase.status.Hard) {
  790. t.Errorf("test: %s, status hard lengths do not match", testName)
  791. }
  792. if len(usage.Status.Used) != len(testCase.status.Used) {
  793. t.Errorf("test: %s, status used lengths do not match", testName)
  794. }
  795. for k, v := range testCase.status.Hard {
  796. actual := usage.Status.Hard[k]
  797. actualValue := actual.String()
  798. expectedValue := v.String()
  799. if expectedValue != actualValue {
  800. t.Errorf("test: %s, Usage Hard: Key: %v, Expected: %v, Actual: %v", testName, k, expectedValue, actualValue)
  801. }
  802. }
  803. for k, v := range testCase.status.Used {
  804. actual := usage.Status.Used[k]
  805. actualValue := actual.String()
  806. expectedValue := v.String()
  807. if expectedValue != actualValue {
  808. t.Errorf("test: %s, Usage Used: Key: %v, Expected: %v, Actual: %v", testName, k, expectedValue, actualValue)
  809. }
  810. }
  811. }
  812. }
  813. func TestAddQuota(t *testing.T) {
  814. kubeClient := fake.NewSimpleClientset()
  815. gvr := v1.SchemeGroupVersion.WithResource("pods")
  816. listersForResourceConfig := map[schema.GroupVersionResource]cache.GenericLister{
  817. gvr: newGenericLister(gvr.GroupResource(), newTestPods()),
  818. }
  819. qc := setupQuotaController(t, kubeClient, mockListerForResourceFunc(listersForResourceConfig), mockDiscoveryFunc)
  820. defer close(qc.stop)
  821. testCases := []struct {
  822. name string
  823. quota *v1.ResourceQuota
  824. expectedPriority bool
  825. }{
  826. {
  827. name: "no status",
  828. expectedPriority: true,
  829. quota: &v1.ResourceQuota{
  830. ObjectMeta: metav1.ObjectMeta{
  831. Namespace: "default",
  832. Name: "rq",
  833. },
  834. Spec: v1.ResourceQuotaSpec{
  835. Hard: v1.ResourceList{
  836. v1.ResourceCPU: resource.MustParse("4"),
  837. },
  838. },
  839. },
  840. },
  841. {
  842. name: "status, no usage",
  843. expectedPriority: true,
  844. quota: &v1.ResourceQuota{
  845. ObjectMeta: metav1.ObjectMeta{
  846. Namespace: "default",
  847. Name: "rq",
  848. },
  849. Spec: v1.ResourceQuotaSpec{
  850. Hard: v1.ResourceList{
  851. v1.ResourceCPU: resource.MustParse("4"),
  852. },
  853. },
  854. Status: v1.ResourceQuotaStatus{
  855. Hard: v1.ResourceList{
  856. v1.ResourceCPU: resource.MustParse("4"),
  857. },
  858. },
  859. },
  860. },
  861. {
  862. name: "status, no usage(to validate it works for extended resources)",
  863. expectedPriority: true,
  864. quota: &v1.ResourceQuota{
  865. ObjectMeta: metav1.ObjectMeta{
  866. Namespace: "default",
  867. Name: "rq",
  868. },
  869. Spec: v1.ResourceQuotaSpec{
  870. Hard: v1.ResourceList{
  871. "requests.example/foobars.example.com": resource.MustParse("4"),
  872. },
  873. },
  874. Status: v1.ResourceQuotaStatus{
  875. Hard: v1.ResourceList{
  876. "requests.example/foobars.example.com": resource.MustParse("4"),
  877. },
  878. },
  879. },
  880. },
  881. {
  882. name: "status, mismatch",
  883. expectedPriority: true,
  884. quota: &v1.ResourceQuota{
  885. ObjectMeta: metav1.ObjectMeta{
  886. Namespace: "default",
  887. Name: "rq",
  888. },
  889. Spec: v1.ResourceQuotaSpec{
  890. Hard: v1.ResourceList{
  891. v1.ResourceCPU: resource.MustParse("4"),
  892. },
  893. },
  894. Status: v1.ResourceQuotaStatus{
  895. Hard: v1.ResourceList{
  896. v1.ResourceCPU: resource.MustParse("6"),
  897. },
  898. Used: v1.ResourceList{
  899. v1.ResourceCPU: resource.MustParse("0"),
  900. },
  901. },
  902. },
  903. },
  904. {
  905. name: "status, missing usage, but don't care (no informer)",
  906. expectedPriority: false,
  907. quota: &v1.ResourceQuota{
  908. ObjectMeta: metav1.ObjectMeta{
  909. Namespace: "default",
  910. Name: "rq",
  911. },
  912. Spec: v1.ResourceQuotaSpec{
  913. Hard: v1.ResourceList{
  914. "foobars.example.com": resource.MustParse("4"),
  915. },
  916. },
  917. Status: v1.ResourceQuotaStatus{
  918. Hard: v1.ResourceList{
  919. "foobars.example.com": resource.MustParse("4"),
  920. },
  921. },
  922. },
  923. },
  924. {
  925. name: "ready",
  926. expectedPriority: false,
  927. quota: &v1.ResourceQuota{
  928. ObjectMeta: metav1.ObjectMeta{
  929. Namespace: "default",
  930. Name: "rq",
  931. },
  932. Spec: v1.ResourceQuotaSpec{
  933. Hard: v1.ResourceList{
  934. v1.ResourceCPU: resource.MustParse("4"),
  935. },
  936. },
  937. Status: v1.ResourceQuotaStatus{
  938. Hard: v1.ResourceList{
  939. v1.ResourceCPU: resource.MustParse("4"),
  940. },
  941. Used: v1.ResourceList{
  942. v1.ResourceCPU: resource.MustParse("0"),
  943. },
  944. },
  945. },
  946. },
  947. }
  948. for _, tc := range testCases {
  949. qc.addQuota(tc.quota)
  950. if tc.expectedPriority {
  951. if e, a := 1, qc.missingUsageQueue.Len(); e != a {
  952. t.Errorf("%s: expected %v, got %v", tc.name, e, a)
  953. }
  954. if e, a := 0, qc.queue.Len(); e != a {
  955. t.Errorf("%s: expected %v, got %v", tc.name, e, a)
  956. }
  957. } else {
  958. if e, a := 0, qc.missingUsageQueue.Len(); e != a {
  959. t.Errorf("%s: expected %v, got %v", tc.name, e, a)
  960. }
  961. if e, a := 1, qc.queue.Len(); e != a {
  962. t.Errorf("%s: expected %v, got %v", tc.name, e, a)
  963. }
  964. }
  965. for qc.missingUsageQueue.Len() > 0 {
  966. key, _ := qc.missingUsageQueue.Get()
  967. qc.missingUsageQueue.Done(key)
  968. }
  969. for qc.queue.Len() > 0 {
  970. key, _ := qc.queue.Get()
  971. qc.queue.Done(key)
  972. }
  973. }
  974. }
  975. // TestDiscoverySync ensures that a discovery client error
  976. // will not cause the quota controller to block infinitely.
  977. func TestDiscoverySync(t *testing.T) {
  978. serverResources := []*metav1.APIResourceList{
  979. {
  980. GroupVersion: "v1",
  981. APIResources: []metav1.APIResource{
  982. {Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
  983. },
  984. },
  985. }
  986. unsyncableServerResources := []*metav1.APIResourceList{
  987. {
  988. GroupVersion: "v1",
  989. APIResources: []metav1.APIResource{
  990. {Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
  991. {Name: "secrets", Namespaced: true, Kind: "Secret", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
  992. },
  993. },
  994. }
  995. fakeDiscoveryClient := &fakeServerResources{
  996. PreferredResources: serverResources,
  997. Error: nil,
  998. Lock: sync.Mutex{},
  999. InterfaceUsedCount: 0,
  1000. }
  1001. testHandler := &fakeActionHandler{
  1002. response: map[string]FakeResponse{
  1003. "GET" + "/api/v1/pods": {
  1004. 200,
  1005. []byte("{}"),
  1006. },
  1007. "GET" + "/api/v1/secrets": {
  1008. 404,
  1009. []byte("{}"),
  1010. },
  1011. },
  1012. }
  1013. srv, clientConfig := testServerAndClientConfig(testHandler.ServeHTTP)
  1014. defer srv.Close()
  1015. clientConfig.ContentConfig.NegotiatedSerializer = nil
  1016. kubeClient, err := kubernetes.NewForConfig(clientConfig)
  1017. if err != nil {
  1018. t.Fatal(err)
  1019. }
  1020. pods := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
  1021. secrets := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}
  1022. listersForResourceConfig := map[schema.GroupVersionResource]cache.GenericLister{
  1023. pods: newGenericLister(pods.GroupResource(), []runtime.Object{}),
  1024. secrets: newGenericLister(secrets.GroupResource(), []runtime.Object{}),
  1025. }
  1026. qc := setupQuotaController(t, kubeClient, mockListerForResourceFunc(listersForResourceConfig), fakeDiscoveryClient.ServerPreferredNamespacedResources)
  1027. defer close(qc.stop)
  1028. stopSync := make(chan struct{})
  1029. defer close(stopSync)
  1030. // The pseudo-code of Sync():
  1031. // Sync(client, period, stopCh):
  1032. // wait.Until() loops with `period` until the `stopCh` is closed :
  1033. // GetQuotableResources()
  1034. // resyncMonitors()
  1035. // controller.WaitForCacheSync() loops with `syncedPollPeriod` (hardcoded to 100ms), until either its stop channel is closed after `period`, or all caches synced.
  1036. //
  1037. // Setting the period to 200ms allows the WaitForCacheSync() to check
  1038. // for cache sync ~2 times in every wait.Until() loop.
  1039. //
  1040. // The 1s sleep in the test allows GetQuotableResources and
  1041. // resyncMonitors to run ~5 times to ensure the changes to the
  1042. // fakeDiscoveryClient are picked up.
  1043. go qc.Sync(fakeDiscoveryClient.ServerPreferredNamespacedResources, 200*time.Millisecond, stopSync)
  1044. // Wait until the sync discovers the initial resources
  1045. time.Sleep(1 * time.Second)
  1046. err = expectSyncNotBlocked(fakeDiscoveryClient, &qc.workerLock)
  1047. if err != nil {
  1048. t.Fatalf("Expected quotacontroller.Sync to be running but it is blocked: %v", err)
  1049. }
  1050. // Simulate the discovery client returning an error
  1051. fakeDiscoveryClient.setPreferredResources(nil)
  1052. fakeDiscoveryClient.setError(fmt.Errorf("Error calling discoveryClient.ServerPreferredResources()"))
  1053. // Wait until sync discovers the change
  1054. time.Sleep(1 * time.Second)
  1055. // Remove the error from being returned and see if the quota sync is still working
  1056. fakeDiscoveryClient.setPreferredResources(serverResources)
  1057. fakeDiscoveryClient.setError(nil)
  1058. err = expectSyncNotBlocked(fakeDiscoveryClient, &qc.workerLock)
  1059. if err != nil {
  1060. t.Fatalf("Expected quotacontroller.Sync to still be running but it is blocked: %v", err)
  1061. }
  1062. // Simulate the discovery client returning a resource the restmapper can resolve, but will not sync caches
  1063. fakeDiscoveryClient.setPreferredResources(unsyncableServerResources)
  1064. fakeDiscoveryClient.setError(nil)
  1065. // Wait until sync discovers the change
  1066. time.Sleep(1 * time.Second)
  1067. // Put the resources back to normal and ensure quota sync recovers
  1068. fakeDiscoveryClient.setPreferredResources(serverResources)
  1069. fakeDiscoveryClient.setError(nil)
  1070. err = expectSyncNotBlocked(fakeDiscoveryClient, &qc.workerLock)
  1071. if err != nil {
  1072. t.Fatalf("Expected quotacontroller.Sync to still be running but it is blocked: %v", err)
  1073. }
  1074. }
  1075. // testServerAndClientConfig returns a server that listens and a config that can reference it
  1076. func testServerAndClientConfig(handler func(http.ResponseWriter, *http.Request)) (*httptest.Server, *rest.Config) {
  1077. srv := httptest.NewServer(http.HandlerFunc(handler))
  1078. config := &rest.Config{
  1079. Host: srv.URL,
  1080. }
  1081. return srv, config
  1082. }
  1083. func expectSyncNotBlocked(fakeDiscoveryClient *fakeServerResources, workerLock *sync.RWMutex) error {
  1084. before := fakeDiscoveryClient.getInterfaceUsedCount()
  1085. t := 1 * time.Second
  1086. time.Sleep(t)
  1087. after := fakeDiscoveryClient.getInterfaceUsedCount()
  1088. if before == after {
  1089. return fmt.Errorf("discoveryClient.ServerPreferredResources() called %d times over %v", after-before, t)
  1090. }
  1091. workerLockAcquired := make(chan struct{})
  1092. go func() {
  1093. workerLock.Lock()
  1094. workerLock.Unlock()
  1095. close(workerLockAcquired)
  1096. }()
  1097. select {
  1098. case <-workerLockAcquired:
  1099. return nil
  1100. case <-time.After(t):
  1101. return fmt.Errorf("workerLock blocked for at least %v", t)
  1102. }
  1103. }
  1104. type fakeServerResources struct {
  1105. PreferredResources []*metav1.APIResourceList
  1106. Error error
  1107. Lock sync.Mutex
  1108. InterfaceUsedCount int
  1109. }
  1110. func (_ *fakeServerResources) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
  1111. return nil, nil
  1112. }
  1113. func (_ *fakeServerResources) ServerResources() ([]*metav1.APIResourceList, error) {
  1114. return nil, nil
  1115. }
  1116. func (_ *fakeServerResources) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
  1117. return nil, nil
  1118. }
  1119. func (f *fakeServerResources) setPreferredResources(resources []*metav1.APIResourceList) {
  1120. f.Lock.Lock()
  1121. defer f.Lock.Unlock()
  1122. f.PreferredResources = resources
  1123. }
  1124. func (f *fakeServerResources) setError(err error) {
  1125. f.Lock.Lock()
  1126. defer f.Lock.Unlock()
  1127. f.Error = err
  1128. }
  1129. func (f *fakeServerResources) getInterfaceUsedCount() int {
  1130. f.Lock.Lock()
  1131. defer f.Lock.Unlock()
  1132. return f.InterfaceUsedCount
  1133. }
  1134. func (f *fakeServerResources) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
  1135. f.Lock.Lock()
  1136. defer f.Lock.Unlock()
  1137. f.InterfaceUsedCount++
  1138. return f.PreferredResources, f.Error
  1139. }
  1140. // fakeAction records information about requests to aid in testing.
  1141. type fakeAction struct {
  1142. method string
  1143. path string
  1144. query string
  1145. }
  1146. // String returns method=path to aid in testing
  1147. func (f *fakeAction) String() string {
  1148. return strings.Join([]string{f.method, f.path}, "=")
  1149. }
  1150. type FakeResponse struct {
  1151. statusCode int
  1152. content []byte
  1153. }
  1154. // fakeActionHandler holds a list of fakeActions received
  1155. type fakeActionHandler struct {
  1156. // statusCode and content returned by this handler for different method + path.
  1157. response map[string]FakeResponse
  1158. lock sync.Mutex
  1159. actions []fakeAction
  1160. }
  1161. // ServeHTTP logs the action that occurred and always returns the associated status code
  1162. func (f *fakeActionHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
  1163. func() {
  1164. f.lock.Lock()
  1165. defer f.lock.Unlock()
  1166. f.actions = append(f.actions, fakeAction{method: request.Method, path: request.URL.Path, query: request.URL.RawQuery})
  1167. fakeResponse, ok := f.response[request.Method+request.URL.Path]
  1168. if !ok {
  1169. fakeResponse.statusCode = 200
  1170. fakeResponse.content = []byte("{\"kind\": \"List\"}")
  1171. }
  1172. response.Header().Set("Content-Type", "application/json")
  1173. response.WriteHeader(fakeResponse.statusCode)
  1174. response.Write(fakeResponse.content)
  1175. }()
  1176. // This is to allow the fakeActionHandler to simulate a watch being opened
  1177. if strings.Contains(request.URL.RawQuery, "watch=true") {
  1178. hijacker, ok := response.(http.Hijacker)
  1179. if !ok {
  1180. return
  1181. }
  1182. connection, _, err := hijacker.Hijack()
  1183. if err != nil {
  1184. return
  1185. }
  1186. defer connection.Close()
  1187. time.Sleep(30 * time.Second)
  1188. }
  1189. }