/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resourcequota import ( "fmt" "strings" "testing" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" core "k8s.io/client-go/testing" "k8s.io/client-go/tools/cache" "k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota/generic" "k8s.io/kubernetes/pkg/quota/install" ) func getResourceList(cpu, memory string) v1.ResourceList { res := v1.ResourceList{} if cpu != "" { res[v1.ResourceCPU] = resource.MustParse(cpu) } if memory != "" { res[v1.ResourceMemory] = resource.MustParse(memory) } return res } func getResourceRequirements(requests, limits v1.ResourceList) v1.ResourceRequirements { res := v1.ResourceRequirements{} res.Requests = requests res.Limits = limits return res } func mockDiscoveryFunc() ([]*metav1.APIResourceList, error) { return []*metav1.APIResourceList{}, nil } func mockListerForResourceFunc(listersForResource map[schema.GroupVersionResource]cache.GenericLister) quota.ListerForResourceFunc { return func(gvr schema.GroupVersionResource) (cache.GenericLister, error) { lister, found := listersForResource[gvr] if !found { return nil, fmt.Errorf("no lister found for resource") } return lister, nil } } func newGenericLister(groupResource schema.GroupResource, items []runtime.Object) cache.GenericLister { store := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) for _, item := range items { store.Add(item) } return cache.NewGenericLister(store, groupResource) } type quotaController struct { *ResourceQuotaController stop chan struct{} } func setupQuotaController(t *testing.T, kubeClient kubernetes.Interface, lister quota.ListerForResourceFunc) quotaController { informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc()) quotaConfiguration := install.NewQuotaConfigurationForControllers(lister) alwaysStarted := make(chan struct{}) close(alwaysStarted) resourceQuotaControllerOptions := &ResourceQuotaControllerOptions{ QuotaClient: kubeClient.CoreV1(), ResourceQuotaInformer: informerFactory.Core().V1().ResourceQuotas(), ResyncPeriod: controller.NoResyncPeriodFunc, ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc, IgnoredResourcesFunc: quotaConfiguration.IgnoredResources, DiscoveryFunc: mockDiscoveryFunc, Registry: generic.NewRegistry(quotaConfiguration.Evaluators()), InformersStarted: alwaysStarted, } qc, err := NewResourceQuotaController(resourceQuotaControllerOptions) if err != nil { t.Fatal(err) } stop := make(chan struct{}) go informerFactory.Start(stop) return quotaController{qc, stop} } func newTestPods() []runtime.Object { return []runtime.Object{ &v1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod-running", Namespace: "testing"}, Status: v1.PodStatus{Phase: v1.PodRunning}, Spec: v1.PodSpec{ Volumes: []v1.Volume{{Name: "vol"}}, Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}}, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod-running-2", Namespace: "testing"}, Status: v1.PodStatus{Phase: v1.PodRunning}, Spec: v1.PodSpec{ Volumes: []v1.Volume{{Name: "vol"}}, Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}}, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod-failed", Namespace: "testing"}, Status: v1.PodStatus{Phase: v1.PodFailed}, Spec: v1.PodSpec{ Volumes: []v1.Volume{{Name: "vol"}}, Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}}, }, }, } } func newBestEffortTestPods() []runtime.Object { return []runtime.Object{ &v1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod-running", Namespace: "testing"}, Status: v1.PodStatus{Phase: v1.PodRunning}, Spec: v1.PodSpec{ Volumes: []v1.Volume{{Name: "vol"}}, Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("", ""), getResourceList("", ""))}}, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod-running-2", Namespace: "testing"}, Status: v1.PodStatus{Phase: v1.PodRunning}, Spec: v1.PodSpec{ Volumes: []v1.Volume{{Name: "vol"}}, Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("", ""), getResourceList("", ""))}}, }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod-failed", Namespace: "testing"}, Status: v1.PodStatus{Phase: v1.PodFailed}, Spec: v1.PodSpec{ Volumes: []v1.Volume{{Name: "vol"}}, Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}}, }, }, } } func newTestPodsWithPriorityClasses() []runtime.Object { return []runtime.Object{ &v1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod-running", Namespace: "testing"}, Status: v1.PodStatus{Phase: v1.PodRunning}, Spec: v1.PodSpec{ Volumes: []v1.Volume{{Name: "vol"}}, Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("500m", "50Gi"), getResourceList("", ""))}}, PriorityClassName: "high", }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod-running-2", Namespace: "testing"}, Status: v1.PodStatus{Phase: v1.PodRunning}, Spec: v1.PodSpec{ Volumes: []v1.Volume{{Name: "vol"}}, Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}}, PriorityClassName: "low", }, }, &v1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod-failed", Namespace: "testing"}, Status: v1.PodStatus{Phase: v1.PodFailed}, Spec: v1.PodSpec{ Volumes: []v1.Volume{{Name: "vol"}}, Containers: []v1.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))}}, }, }, } } func TestSyncResourceQuota(t *testing.T) { testCases := map[string]struct { gvr schema.GroupVersionResource items []runtime.Object quota v1.ResourceQuota status v1.ResourceQuotaStatus expectedActionSet sets.String }{ "non-matching-best-effort-scoped-quota": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Scopes: []v1.ResourceQuotaScope{v1.ResourceQuotaScopeBestEffort}, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("0"), v1.ResourceMemory: resource.MustParse("0"), v1.ResourcePods: resource.MustParse("0"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newTestPods(), }, "matching-best-effort-scoped-quota": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Scopes: []v1.ResourceQuotaScope{v1.ResourceQuotaScopeBestEffort}, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("0"), v1.ResourceMemory: resource.MustParse("0"), v1.ResourcePods: resource.MustParse("2"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newBestEffortTestPods(), }, "non-matching-priorityclass-scoped-quota-OpExists": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, ScopeSelector: &v1.ScopeSelector{ MatchExpressions: []v1.ScopedResourceSelectorRequirement{ { ScopeName: v1.ResourceQuotaScopePriorityClass, Operator: v1.ScopeSelectorOpExists}, }, }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("0"), v1.ResourceMemory: resource.MustParse("0"), v1.ResourcePods: resource.MustParse("0"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newTestPods(), }, "matching-priorityclass-scoped-quota-OpExists": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, ScopeSelector: &v1.ScopeSelector{ MatchExpressions: []v1.ScopedResourceSelectorRequirement{ { ScopeName: v1.ResourceQuotaScopePriorityClass, Operator: v1.ScopeSelectorOpExists}, }, }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("600m"), v1.ResourceMemory: resource.MustParse("51Gi"), v1.ResourcePods: resource.MustParse("2"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newTestPodsWithPriorityClasses(), }, "matching-priorityclass-scoped-quota-OpIn": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, ScopeSelector: &v1.ScopeSelector{ MatchExpressions: []v1.ScopedResourceSelectorRequirement{ { ScopeName: v1.ResourceQuotaScopePriorityClass, Operator: v1.ScopeSelectorOpIn, Values: []string{"high", "low"}, }, }, }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("600m"), v1.ResourceMemory: resource.MustParse("51Gi"), v1.ResourcePods: resource.MustParse("2"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newTestPodsWithPriorityClasses(), }, "matching-priorityclass-scoped-quota-OpIn-high": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, ScopeSelector: &v1.ScopeSelector{ MatchExpressions: []v1.ScopedResourceSelectorRequirement{ { ScopeName: v1.ResourceQuotaScopePriorityClass, Operator: v1.ScopeSelectorOpIn, Values: []string{"high"}, }, }, }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("500m"), v1.ResourceMemory: resource.MustParse("50Gi"), v1.ResourcePods: resource.MustParse("1"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newTestPodsWithPriorityClasses(), }, "matching-priorityclass-scoped-quota-OpIn-low": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, ScopeSelector: &v1.ScopeSelector{ MatchExpressions: []v1.ScopedResourceSelectorRequirement{ { ScopeName: v1.ResourceQuotaScopePriorityClass, Operator: v1.ScopeSelectorOpIn, Values: []string{"low"}, }, }, }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("100m"), v1.ResourceMemory: resource.MustParse("1Gi"), v1.ResourcePods: resource.MustParse("1"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newTestPodsWithPriorityClasses(), }, "matching-priorityclass-scoped-quota-OpNotIn-low": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, ScopeSelector: &v1.ScopeSelector{ MatchExpressions: []v1.ScopedResourceSelectorRequirement{ { ScopeName: v1.ResourceQuotaScopePriorityClass, Operator: v1.ScopeSelectorOpNotIn, Values: []string{"high"}, }, }, }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("100m"), v1.ResourceMemory: resource.MustParse("1Gi"), v1.ResourcePods: resource.MustParse("1"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newTestPodsWithPriorityClasses(), }, "non-matching-priorityclass-scoped-quota-OpIn": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, ScopeSelector: &v1.ScopeSelector{ MatchExpressions: []v1.ScopedResourceSelectorRequirement{ { ScopeName: v1.ResourceQuotaScopePriorityClass, Operator: v1.ScopeSelectorOpIn, Values: []string{"random"}, }, }, }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("0"), v1.ResourceMemory: resource.MustParse("0"), v1.ResourcePods: resource.MustParse("0"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newTestPodsWithPriorityClasses(), }, "non-matching-priorityclass-scoped-quota-OpNotIn": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, ScopeSelector: &v1.ScopeSelector{ MatchExpressions: []v1.ScopedResourceSelectorRequirement{ { ScopeName: v1.ResourceQuotaScopePriorityClass, Operator: v1.ScopeSelectorOpNotIn, Values: []string{"random"}, }, }, }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("200m"), v1.ResourceMemory: resource.MustParse("2Gi"), v1.ResourcePods: resource.MustParse("2"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newTestPods(), }, "matching-priorityclass-scoped-quota-OpDoesNotExist": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, ScopeSelector: &v1.ScopeSelector{ MatchExpressions: []v1.ScopedResourceSelectorRequirement{ { ScopeName: v1.ResourceQuotaScopePriorityClass, Operator: v1.ScopeSelectorOpDoesNotExist, }, }, }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("200m"), v1.ResourceMemory: resource.MustParse("2Gi"), v1.ResourcePods: resource.MustParse("2"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newTestPods(), }, "pods": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "testing"}, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), v1.ResourceMemory: resource.MustParse("100Gi"), v1.ResourcePods: resource.MustParse("5"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("200m"), v1.ResourceMemory: resource.MustParse("2Gi"), v1.ResourcePods: resource.MustParse("2"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: newTestPods(), }, "quota-spec-hard-updated": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "rq", }, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("4"), }, }, Status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("3"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("0"), }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("4"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("0"), }, }, expectedActionSet: sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ), items: []runtime.Object{}, }, "quota-unchanged": { gvr: v1.SchemeGroupVersion.WithResource("pods"), quota: v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "rq", }, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("4"), }, }, Status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("0"), }, }, }, status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("4"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("0"), }, }, expectedActionSet: sets.NewString(), items: []runtime.Object{}, }, } for testName, testCase := range testCases { kubeClient := fake.NewSimpleClientset(&testCase.quota) listersForResourceConfig := map[schema.GroupVersionResource]cache.GenericLister{ testCase.gvr: newGenericLister(testCase.gvr.GroupResource(), testCase.items), } qc := setupQuotaController(t, kubeClient, mockListerForResourceFunc(listersForResourceConfig)) defer close(qc.stop) if err := qc.syncResourceQuota(&testCase.quota); err != nil { t.Fatalf("test: %s, unexpected error: %v", testName, err) } actionSet := sets.NewString() for _, action := range kubeClient.Actions() { actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-")) } if !actionSet.HasAll(testCase.expectedActionSet.List()...) { t.Errorf("test: %s,\nExpected actions:\n%v\n but got:\n%v\nDifference:\n%v", testName, testCase.expectedActionSet, actionSet, testCase.expectedActionSet.Difference(actionSet)) } lastActionIndex := len(kubeClient.Actions()) - 1 usage := kubeClient.Actions()[lastActionIndex].(core.UpdateAction).GetObject().(*v1.ResourceQuota) // ensure usage is as expected if len(usage.Status.Hard) != len(testCase.status.Hard) { t.Errorf("test: %s, status hard lengths do not match", testName) } if len(usage.Status.Used) != len(testCase.status.Used) { t.Errorf("test: %s, status used lengths do not match", testName) } for k, v := range testCase.status.Hard { actual := usage.Status.Hard[k] actualValue := actual.String() expectedValue := v.String() if expectedValue != actualValue { t.Errorf("test: %s, Usage Hard: Key: %v, Expected: %v, Actual: %v", testName, k, expectedValue, actualValue) } } for k, v := range testCase.status.Used { actual := usage.Status.Used[k] actualValue := actual.String() expectedValue := v.String() if expectedValue != actualValue { t.Errorf("test: %s, Usage Used: Key: %v, Expected: %v, Actual: %v", testName, k, expectedValue, actualValue) } } } } func TestAddQuota(t *testing.T) { kubeClient := fake.NewSimpleClientset() gvr := v1.SchemeGroupVersion.WithResource("pods") listersForResourceConfig := map[schema.GroupVersionResource]cache.GenericLister{ gvr: newGenericLister(gvr.GroupResource(), newTestPods()), } qc := setupQuotaController(t, kubeClient, mockListerForResourceFunc(listersForResourceConfig)) defer close(qc.stop) testCases := []struct { name string quota *v1.ResourceQuota expectedPriority bool }{ { name: "no status", expectedPriority: true, quota: &v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "rq", }, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("4"), }, }, }, }, { name: "status, no usage", expectedPriority: true, quota: &v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "rq", }, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("4"), }, }, Status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("4"), }, }, }, }, { name: "status, no usage(to validate it works for extended resources)", expectedPriority: true, quota: &v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "rq", }, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ "requests.example/foobars.example.com": resource.MustParse("4"), }, }, Status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ "requests.example/foobars.example.com": resource.MustParse("4"), }, }, }, }, { name: "status, mismatch", expectedPriority: true, quota: &v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "rq", }, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("4"), }, }, Status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("6"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("0"), }, }, }, }, { name: "status, missing usage, but don't care (no informer)", expectedPriority: false, quota: &v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "rq", }, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ "foobars.example.com": resource.MustParse("4"), }, }, Status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ "foobars.example.com": resource.MustParse("4"), }, }, }, }, { name: "ready", expectedPriority: false, quota: &v1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "rq", }, Spec: v1.ResourceQuotaSpec{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("4"), }, }, Status: v1.ResourceQuotaStatus{ Hard: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("4"), }, Used: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("0"), }, }, }, }, } for _, tc := range testCases { qc.addQuota(tc.quota) if tc.expectedPriority { if e, a := 1, qc.missingUsageQueue.Len(); e != a { t.Errorf("%s: expected %v, got %v", tc.name, e, a) } if e, a := 0, qc.queue.Len(); e != a { t.Errorf("%s: expected %v, got %v", tc.name, e, a) } } else { if e, a := 0, qc.missingUsageQueue.Len(); e != a { t.Errorf("%s: expected %v, got %v", tc.name, e, a) } if e, a := 1, qc.queue.Len(); e != a { t.Errorf("%s: expected %v, got %v", tc.name, e, a) } } for qc.missingUsageQueue.Len() > 0 { key, _ := qc.missingUsageQueue.Get() qc.missingUsageQueue.Done(key) } for qc.queue.Len() > 0 { key, _ := qc.queue.Get() qc.queue.Done(key) } } }