k8s.io/apiserver@v0.31.1/pkg/admission/plugin/resourcequota/resource_access_test.go (about) 1 /* 2 Copyright 2020 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 "fmt" 21 "reflect" 22 "sync" 23 "sync/atomic" 24 "testing" 25 "time" 26 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/runtime" 29 30 corev1 "k8s.io/api/core/v1" 31 "k8s.io/client-go/informers" 32 "k8s.io/client-go/kubernetes/fake" 33 core "k8s.io/client-go/testing" 34 "k8s.io/utils/lru" 35 ) 36 37 func TestLRUCacheLookup(t *testing.T) { 38 namespace := "foo" 39 resourceQuota := &corev1.ResourceQuota{ 40 ObjectMeta: metav1.ObjectMeta{ 41 Name: "foo", 42 Namespace: namespace, 43 }, 44 } 45 46 testcases := []struct { 47 description string 48 cacheInput []*corev1.ResourceQuota 49 clientInput []runtime.Object 50 ttl time.Duration 51 namespace string 52 expectedQuota *corev1.ResourceQuota 53 }{ 54 { 55 description: "object is found via cache", 56 cacheInput: []*corev1.ResourceQuota{resourceQuota}, 57 ttl: 30 * time.Second, 58 namespace: namespace, 59 expectedQuota: resourceQuota, 60 }, 61 { 62 description: "object is outdated and not found with client", 63 cacheInput: []*corev1.ResourceQuota{resourceQuota}, 64 ttl: -30 * time.Second, 65 namespace: namespace, 66 expectedQuota: nil, 67 }, 68 { 69 description: "object is outdated but is found with client", 70 cacheInput: []*corev1.ResourceQuota{resourceQuota}, 71 clientInput: []runtime.Object{resourceQuota}, 72 ttl: -30 * time.Second, 73 namespace: namespace, 74 expectedQuota: resourceQuota, 75 }, 76 { 77 description: "object does not exist in cache and is not found with client", 78 cacheInput: []*corev1.ResourceQuota{resourceQuota}, 79 ttl: 30 * time.Second, 80 expectedQuota: nil, 81 }, 82 { 83 description: "object does not exist in cache and is found with client", 84 cacheInput: []*corev1.ResourceQuota{}, 85 clientInput: []runtime.Object{resourceQuota}, 86 namespace: namespace, 87 expectedQuota: resourceQuota, 88 }, 89 } 90 91 for _, tc := range testcases { 92 t.Run(tc.description, func(t *testing.T) { 93 liveLookupCache := lru.New(1) 94 kubeClient := fake.NewSimpleClientset(tc.clientInput...) 95 informerFactory := informers.NewSharedInformerFactory(kubeClient, 0) 96 97 accessor, _ := newQuotaAccessor() 98 accessor.client = kubeClient 99 accessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister() 100 accessor.liveLookupCache = liveLookupCache 101 102 for _, q := range tc.cacheInput { 103 quota := q 104 liveLookupCache.Add(quota.Namespace, liveLookupEntry{expiry: time.Now().Add(tc.ttl), items: []*corev1.ResourceQuota{quota}}) 105 } 106 107 quotas, err := accessor.GetQuotas(tc.namespace) 108 if err != nil { 109 t.Errorf("Unexpected error: %v", err) 110 } 111 112 if tc.expectedQuota != nil { 113 if count := len(quotas); count != 1 { 114 t.Fatalf("Expected 1 object but got %d", count) 115 } 116 117 if !reflect.DeepEqual(quotas[0], *tc.expectedQuota) { 118 t.Errorf("Retrieved object does not match") 119 } 120 return 121 } 122 123 if count := len(quotas); count > 0 { 124 t.Errorf("Expected 0 objects but got %d", count) 125 } 126 }) 127 } 128 } 129 130 // TestGetQuotas ensures we do not have multiple LIST calls to the apiserver 131 // in-flight at any one time. This is to ensure the issue described in #22422 do 132 // not happen again. 133 func TestGetQuotas(t *testing.T) { 134 var ( 135 testNamespace1 = "test-a" 136 testNamespace2 = "test-b" 137 listCallCountTestNamespace1 int64 138 listCallCountTestNamespace2 int64 139 ) 140 resourceQuota := &corev1.ResourceQuota{ 141 ObjectMeta: metav1.ObjectMeta{ 142 Name: "foo", 143 }, 144 } 145 146 resourceQuotas := []*corev1.ResourceQuota{resourceQuota} 147 148 kubeClient := &fake.Clientset{} 149 informerFactory := informers.NewSharedInformerFactory(kubeClient, 0) 150 151 accessor, _ := newQuotaAccessor() 152 accessor.client = kubeClient 153 accessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister() 154 155 kubeClient.AddReactor("list", "resourcequotas", func(action core.Action) (bool, runtime.Object, error) { 156 switch action.GetNamespace() { 157 case testNamespace1: 158 atomic.AddInt64(&listCallCountTestNamespace1, 1) 159 case testNamespace2: 160 atomic.AddInt64(&listCallCountTestNamespace2, 1) 161 default: 162 t.Error("unexpected namespace") 163 } 164 165 resourceQuotaList := &corev1.ResourceQuotaList{ 166 ListMeta: metav1.ListMeta{ 167 ResourceVersion: fmt.Sprintf("%d", len(resourceQuotas)), 168 }, 169 } 170 for i, quota := range resourceQuotas { 171 quota.ResourceVersion = fmt.Sprintf("%d", i) 172 quota.Namespace = action.GetNamespace() 173 resourceQuotaList.Items = append(resourceQuotaList.Items, *quota) 174 } 175 // make the handler slow so concurrent calls exercise the singleflight 176 time.Sleep(time.Second) 177 return true, resourceQuotaList, nil 178 }) 179 180 wg := sync.WaitGroup{} 181 for i := 0; i < 10; i++ { 182 wg.Add(2) 183 // simulating concurrent calls after a cache failure 184 go func() { 185 defer wg.Done() 186 quotas, err := accessor.GetQuotas(testNamespace1) 187 if err != nil { 188 t.Errorf("unexpected error: %v", err) 189 } 190 if len(quotas) != len(resourceQuotas) { 191 t.Errorf("Expected %d resource quotas, got %d", len(resourceQuotas), len(quotas)) 192 } 193 for _, q := range quotas { 194 if q.Namespace != testNamespace1 { 195 t.Errorf("Expected %s namespace, got %s", testNamespace1, q.Namespace) 196 } 197 } 198 }() 199 200 // simulation of different namespaces is a call for a different group key, but not shared with the first namespace 201 go func() { 202 defer wg.Done() 203 quotas, err := accessor.GetQuotas(testNamespace2) 204 if err != nil { 205 t.Errorf("unexpected error: %v", err) 206 } 207 if len(quotas) != len(resourceQuotas) { 208 t.Errorf("Expected %d resource quotas, got %d", len(resourceQuotas), len(quotas)) 209 } 210 for _, q := range quotas { 211 if q.Namespace != testNamespace2 { 212 t.Errorf("Expected %s namespace, got %s", testNamespace2, q.Namespace) 213 } 214 } 215 }() 216 } 217 218 // and here we wait for all the goroutines 219 wg.Wait() 220 // since all the calls with the same namespace will be held, they must 221 // be caught on the singleflight group. there are two different sets of 222 // namespace calls hence only 2. 223 if listCallCountTestNamespace1 != 1 { 224 t.Errorf("Expected 1 resource quota call, got %d", listCallCountTestNamespace1) 225 } 226 if listCallCountTestNamespace2 != 1 { 227 t.Errorf("Expected 1 resource quota call, got %d", listCallCountTestNamespace2) 228 } 229 230 // invalidate the cache 231 accessor.liveLookupCache.Remove(testNamespace1) 232 quotas, err := accessor.GetQuotas(testNamespace1) 233 if err != nil { 234 t.Errorf("unexpected error: %v", err) 235 } 236 if len(quotas) != len(resourceQuotas) { 237 t.Errorf("Expected %d resource quotas, got %d", len(resourceQuotas), len(quotas)) 238 } 239 240 if listCallCountTestNamespace1 != 2 { 241 t.Errorf("Expected 2 resource quota call, got %d", listCallCountTestNamespace1) 242 } 243 if listCallCountTestNamespace2 != 1 { 244 t.Errorf("Expected 1 resource quota call, got %d", listCallCountTestNamespace2) 245 } 246 }