k8s.io/kubernetes@v1.29.3/pkg/controller/resourcequota/resource_quota_controller_test.go (about) 1 /* 2 Copyright 2015 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 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 // ensure usage is as expected 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 // TestDiscoverySync ensures that a discovery client error 1011 // will not cause the quota controller to block infinitely. 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 // The pseudo-code of Sync(): 1092 // Sync(client, period, stopCh): 1093 // wait.Until() loops with `period` until the `stopCh` is closed : 1094 // GetQuotableResources() 1095 // resyncMonitors() 1096 // cache.WaitForNamedCacheSync() loops with `syncedPollPeriod` (hardcoded to 100ms), until either its stop channel is closed after `period`, or all caches synced. 1097 // 1098 // Setting the period to 200ms allows the WaitForCacheSync() to check 1099 // for cache sync ~2 times in every wait.Until() loop. 1100 // 1101 // The 1s sleep in the test allows GetQuotableResources and 1102 // resyncMonitors to run ~5 times to ensure the changes to the 1103 // fakeDiscoveryClient are picked up. 1104 _, ctx := ktesting.NewTestContext(t) 1105 go qc.Sync(ctx, fakeDiscoveryClient.ServerPreferredNamespacedResources, 200*time.Millisecond) 1106 1107 // Wait until the sync discovers the initial resources 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 // Simulate the discovery client returning an error 1117 fakeDiscoveryClient.setPreferredResources(nil, fmt.Errorf("error calling discoveryClient.ServerPreferredResources()")) 1118 1119 // Wait until sync discovers the change 1120 time.Sleep(1 * time.Second) 1121 // No monitors removed 1122 assertMonitors(t, qc, "pods", "deployments") 1123 1124 // Remove the error from being returned and see if the quota sync is still working 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 // Simulate the discovery client returning a resource the restmapper can resolve, but will not sync caches 1134 fakeDiscoveryClient.setPreferredResources(unsyncableServerResources, nil) 1135 1136 // Wait until sync discovers the change 1137 time.Sleep(1 * time.Second) 1138 // deployments removed, secrets added 1139 assertMonitors(t, qc, "pods", "secrets") 1140 1141 // Put the resources back to normal and ensure quota sync recovers 1142 fakeDiscoveryClient.setPreferredResources(serverResources, nil) 1143 1144 // Wait until sync discovers the change 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 // secrets removed, deployments readded 1151 assertMonitors(t, qc, "pods", "deployments") 1152 1153 // apps/v1 discovery failure 1154 fakeDiscoveryClient.setPreferredResources(unsyncableServerResources, appsV1Error) 1155 // Wait until sync discovers the change 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 // deployments remain due to appsv1 error, secrets added 1162 assertMonitors(t, qc, "pods", "deployments", "secrets") 1163 1164 // core/v1 discovery failure 1165 fakeDiscoveryClient.setPreferredResources(appsV1Resources, coreV1Error) 1166 // Wait until sync discovers the change 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 // pods and secrets remain due to corev1 error 1173 assertMonitors(t, qc, "pods", "deployments", "secrets") 1174 1175 // Put the resources back to normal and ensure quota sync recovers 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 // secrets removed, deployments remain 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 // testServerAndClientConfig returns a server that listens and a config that can reference it 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 // fakeAction records information about requests to aid in testing. 1266 type fakeAction struct { 1267 method string 1268 path string 1269 query string 1270 } 1271 1272 // String returns method=path to aid in testing 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 // fakeActionHandler holds a list of fakeActions received 1283 type fakeActionHandler struct { 1284 // statusCode and content returned by this handler for different method + path. 1285 response map[string]FakeResponse 1286 1287 lock sync.Mutex 1288 actions []fakeAction 1289 } 1290 1291 // ServeHTTP logs the action that occurred and always returns the associated status code 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 // This is to allow the fakeActionHandler to simulate a watch being opened 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 }