12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253 |
- /*
- 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"
- "net/http"
- "net/http/httptest"
- "strings"
- "sync"
- "testing"
- "time"
- "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/resource"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/labels"
- "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"
- "k8s.io/client-go/rest"
- core "k8s.io/client-go/testing"
- "k8s.io/client-go/tools/cache"
- "k8s.io/kubernetes/pkg/controller"
- quota "k8s.io/kubernetes/pkg/quota/v1"
- "k8s.io/kubernetes/pkg/quota/v1/generic"
- "k8s.io/kubernetes/pkg/quota/v1/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)
- }
- func newErrorLister() cache.GenericLister {
- return errorLister{}
- }
- type errorLister struct {
- }
- func (errorLister) List(selector labels.Selector) (ret []runtime.Object, err error) {
- return nil, fmt.Errorf("error listing")
- }
- func (errorLister) Get(name string) (runtime.Object, error) {
- return nil, fmt.Errorf("error getting")
- }
- func (errorLister) ByNamespace(namespace string) cache.GenericNamespaceLister {
- return errorLister{}
- }
- type quotaController struct {
- *ResourceQuotaController
- stop chan struct{}
- }
- func setupQuotaController(t *testing.T, kubeClient kubernetes.Interface, lister quota.ListerForResourceFunc, discoveryFunc NamespacedResourcesFunc) 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: discoveryFunc,
- Registry: generic.NewRegistry(quotaConfiguration.Evaluators()),
- InformersStarted: alwaysStarted,
- InformerFactory: informerFactory,
- }
- qc, err := NewResourceQuotaController(resourceQuotaControllerOptions)
- if err != nil {
- t.Fatal(err)
- }
- stop := make(chan struct{})
- 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
- errorGVR schema.GroupVersionResource
- items []runtime.Object
- quota v1.ResourceQuota
- status v1.ResourceQuotaStatus
- expectedError string
- 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{},
- },
- "quota-missing-status-with-calculation-error": {
- errorGVR: v1.SchemeGroupVersion.WithResource("pods"),
- quota: v1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{
- Namespace: "default",
- Name: "rq",
- },
- Spec: v1.ResourceQuotaSpec{
- Hard: v1.ResourceList{
- v1.ResourcePods: resource.MustParse("1"),
- },
- },
- Status: v1.ResourceQuotaStatus{},
- },
- status: v1.ResourceQuotaStatus{
- Hard: v1.ResourceList{
- v1.ResourcePods: resource.MustParse("1"),
- },
- },
- expectedError: "error listing",
- expectedActionSet: sets.NewString("update-resourcequotas-status"),
- items: []runtime.Object{},
- },
- "quota-missing-status-with-partial-calculation-error": {
- gvr: v1.SchemeGroupVersion.WithResource("configmaps"),
- errorGVR: v1.SchemeGroupVersion.WithResource("pods"),
- quota: v1.ResourceQuota{
- ObjectMeta: metav1.ObjectMeta{
- Namespace: "default",
- Name: "rq",
- },
- Spec: v1.ResourceQuotaSpec{
- Hard: v1.ResourceList{
- v1.ResourcePods: resource.MustParse("1"),
- v1.ResourceConfigMaps: resource.MustParse("1"),
- },
- },
- Status: v1.ResourceQuotaStatus{},
- },
- status: v1.ResourceQuotaStatus{
- Hard: v1.ResourceList{
- v1.ResourcePods: resource.MustParse("1"),
- v1.ResourceConfigMaps: resource.MustParse("1"),
- },
- Used: v1.ResourceList{
- v1.ResourceConfigMaps: resource.MustParse("0"),
- },
- },
- expectedError: "error listing",
- expectedActionSet: sets.NewString("update-resourcequotas-status"),
- 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),
- testCase.errorGVR: newErrorLister(),
- }
- qc := setupQuotaController(t, kubeClient, mockListerForResourceFunc(listersForResourceConfig), mockDiscoveryFunc)
- defer close(qc.stop)
- if err := qc.syncResourceQuota(&testCase.quota); err != nil {
- if len(testCase.expectedError) == 0 || !strings.Contains(err.Error(), testCase.expectedError) {
- t.Fatalf("test: %s, unexpected error: %v", testName, err)
- }
- } else if len(testCase.expectedError) > 0 {
- t.Fatalf("test: %s, expected error %q, got none", testName, testCase.expectedError)
- }
- 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))
- }
- var usage *v1.ResourceQuota
- actions := kubeClient.Actions()
- for i := len(actions) - 1; i >= 0; i-- {
- if updateAction, ok := actions[i].(core.UpdateAction); ok {
- usage = updateAction.GetObject().(*v1.ResourceQuota)
- break
- }
- }
- if usage == nil {
- t.Errorf("test: %s,\nExpected update action usage, got none: actions:\n%v", testName, actions)
- }
- // 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), mockDiscoveryFunc)
- 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)
- }
- }
- }
- // TestDiscoverySync ensures that a discovery client error
- // will not cause the quota controller to block infinitely.
- func TestDiscoverySync(t *testing.T) {
- serverResources := []*metav1.APIResourceList{
- {
- GroupVersion: "v1",
- APIResources: []metav1.APIResource{
- {Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
- },
- },
- }
- unsyncableServerResources := []*metav1.APIResourceList{
- {
- GroupVersion: "v1",
- APIResources: []metav1.APIResource{
- {Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
- {Name: "secrets", Namespaced: true, Kind: "Secret", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
- },
- },
- }
- fakeDiscoveryClient := &fakeServerResources{
- PreferredResources: serverResources,
- Error: nil,
- Lock: sync.Mutex{},
- InterfaceUsedCount: 0,
- }
- testHandler := &fakeActionHandler{
- response: map[string]FakeResponse{
- "GET" + "/api/v1/pods": {
- 200,
- []byte("{}"),
- },
- "GET" + "/api/v1/secrets": {
- 404,
- []byte("{}"),
- },
- },
- }
- srv, clientConfig := testServerAndClientConfig(testHandler.ServeHTTP)
- defer srv.Close()
- clientConfig.ContentConfig.NegotiatedSerializer = nil
- kubeClient, err := kubernetes.NewForConfig(clientConfig)
- if err != nil {
- t.Fatal(err)
- }
- pods := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
- secrets := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}
- listersForResourceConfig := map[schema.GroupVersionResource]cache.GenericLister{
- pods: newGenericLister(pods.GroupResource(), []runtime.Object{}),
- secrets: newGenericLister(secrets.GroupResource(), []runtime.Object{}),
- }
- qc := setupQuotaController(t, kubeClient, mockListerForResourceFunc(listersForResourceConfig), fakeDiscoveryClient.ServerPreferredNamespacedResources)
- defer close(qc.stop)
- stopSync := make(chan struct{})
- defer close(stopSync)
- // The pseudo-code of Sync():
- // Sync(client, period, stopCh):
- // wait.Until() loops with `period` until the `stopCh` is closed :
- // GetQuotableResources()
- // resyncMonitors()
- // controller.WaitForCacheSync() loops with `syncedPollPeriod` (hardcoded to 100ms), until either its stop channel is closed after `period`, or all caches synced.
- //
- // Setting the period to 200ms allows the WaitForCacheSync() to check
- // for cache sync ~2 times in every wait.Until() loop.
- //
- // The 1s sleep in the test allows GetQuotableResources and
- // resyncMonitors to run ~5 times to ensure the changes to the
- // fakeDiscoveryClient are picked up.
- go qc.Sync(fakeDiscoveryClient.ServerPreferredNamespacedResources, 200*time.Millisecond, stopSync)
- // Wait until the sync discovers the initial resources
- time.Sleep(1 * time.Second)
- err = expectSyncNotBlocked(fakeDiscoveryClient, &qc.workerLock)
- if err != nil {
- t.Fatalf("Expected quotacontroller.Sync to be running but it is blocked: %v", err)
- }
- // Simulate the discovery client returning an error
- fakeDiscoveryClient.setPreferredResources(nil)
- fakeDiscoveryClient.setError(fmt.Errorf("Error calling discoveryClient.ServerPreferredResources()"))
- // Wait until sync discovers the change
- time.Sleep(1 * time.Second)
- // Remove the error from being returned and see if the quota sync is still working
- fakeDiscoveryClient.setPreferredResources(serverResources)
- fakeDiscoveryClient.setError(nil)
- err = expectSyncNotBlocked(fakeDiscoveryClient, &qc.workerLock)
- if err != nil {
- t.Fatalf("Expected quotacontroller.Sync to still be running but it is blocked: %v", err)
- }
- // Simulate the discovery client returning a resource the restmapper can resolve, but will not sync caches
- fakeDiscoveryClient.setPreferredResources(unsyncableServerResources)
- fakeDiscoveryClient.setError(nil)
- // Wait until sync discovers the change
- time.Sleep(1 * time.Second)
- // Put the resources back to normal and ensure quota sync recovers
- fakeDiscoveryClient.setPreferredResources(serverResources)
- fakeDiscoveryClient.setError(nil)
- err = expectSyncNotBlocked(fakeDiscoveryClient, &qc.workerLock)
- if err != nil {
- t.Fatalf("Expected quotacontroller.Sync to still be running but it is blocked: %v", err)
- }
- }
- // testServerAndClientConfig returns a server that listens and a config that can reference it
- func testServerAndClientConfig(handler func(http.ResponseWriter, *http.Request)) (*httptest.Server, *rest.Config) {
- srv := httptest.NewServer(http.HandlerFunc(handler))
- config := &rest.Config{
- Host: srv.URL,
- }
- return srv, config
- }
- func expectSyncNotBlocked(fakeDiscoveryClient *fakeServerResources, workerLock *sync.RWMutex) error {
- before := fakeDiscoveryClient.getInterfaceUsedCount()
- t := 1 * time.Second
- time.Sleep(t)
- after := fakeDiscoveryClient.getInterfaceUsedCount()
- if before == after {
- return fmt.Errorf("discoveryClient.ServerPreferredResources() called %d times over %v", after-before, t)
- }
- workerLockAcquired := make(chan struct{})
- go func() {
- workerLock.Lock()
- workerLock.Unlock()
- close(workerLockAcquired)
- }()
- select {
- case <-workerLockAcquired:
- return nil
- case <-time.After(t):
- return fmt.Errorf("workerLock blocked for at least %v", t)
- }
- }
- type fakeServerResources struct {
- PreferredResources []*metav1.APIResourceList
- Error error
- Lock sync.Mutex
- InterfaceUsedCount int
- }
- func (_ *fakeServerResources) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
- return nil, nil
- }
- func (_ *fakeServerResources) ServerResources() ([]*metav1.APIResourceList, error) {
- return nil, nil
- }
- func (_ *fakeServerResources) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
- return nil, nil
- }
- func (f *fakeServerResources) setPreferredResources(resources []*metav1.APIResourceList) {
- f.Lock.Lock()
- defer f.Lock.Unlock()
- f.PreferredResources = resources
- }
- func (f *fakeServerResources) setError(err error) {
- f.Lock.Lock()
- defer f.Lock.Unlock()
- f.Error = err
- }
- func (f *fakeServerResources) getInterfaceUsedCount() int {
- f.Lock.Lock()
- defer f.Lock.Unlock()
- return f.InterfaceUsedCount
- }
- func (f *fakeServerResources) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
- f.Lock.Lock()
- defer f.Lock.Unlock()
- f.InterfaceUsedCount++
- return f.PreferredResources, f.Error
- }
- // fakeAction records information about requests to aid in testing.
- type fakeAction struct {
- method string
- path string
- query string
- }
- // String returns method=path to aid in testing
- func (f *fakeAction) String() string {
- return strings.Join([]string{f.method, f.path}, "=")
- }
- type FakeResponse struct {
- statusCode int
- content []byte
- }
- // fakeActionHandler holds a list of fakeActions received
- type fakeActionHandler struct {
- // statusCode and content returned by this handler for different method + path.
- response map[string]FakeResponse
- lock sync.Mutex
- actions []fakeAction
- }
- // ServeHTTP logs the action that occurred and always returns the associated status code
- func (f *fakeActionHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
- func() {
- f.lock.Lock()
- defer f.lock.Unlock()
- f.actions = append(f.actions, fakeAction{method: request.Method, path: request.URL.Path, query: request.URL.RawQuery})
- fakeResponse, ok := f.response[request.Method+request.URL.Path]
- if !ok {
- fakeResponse.statusCode = 200
- fakeResponse.content = []byte("{\"kind\": \"List\"}")
- }
- response.Header().Set("Content-Type", "application/json")
- response.WriteHeader(fakeResponse.statusCode)
- response.Write(fakeResponse.content)
- }()
- // This is to allow the fakeActionHandler to simulate a watch being opened
- if strings.Contains(request.URL.RawQuery, "watch=true") {
- hijacker, ok := response.(http.Hijacker)
- if !ok {
- return
- }
- connection, _, err := hijacker.Hijack()
- if err != nil {
- return
- }
- defer connection.Close()
- time.Sleep(30 * time.Second)
- }
- }
|