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