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  }