k8s.io/client-go@v0.31.1/util/consistencydetector/data_consistency_detector_test.go (about)

     1  /*
     2  Copyright 2023 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 consistencydetector
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"testing"
    23  
    24  	"github.com/stretchr/testify/require"
    25  
    26  	v1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	"k8s.io/utils/ptr"
    32  )
    33  
    34  func TestDataConsistencyChecker(t *testing.T) {
    35  	scenarios := []struct {
    36  		name string
    37  
    38  		lastSyncedResourceVersion string
    39  		listResponse              runtime.Object
    40  		retrievedItems            []runtime.Object
    41  		requestOptions            metav1.ListOptions
    42  
    43  		expectedRequestOptions []metav1.ListOptions
    44  		expectedListRequests   int
    45  		expectPanic            bool
    46  	}{
    47  		{
    48  			name:                      "data consistency check won't panic when data is consistent",
    49  			lastSyncedResourceVersion: "2",
    50  			listResponse: &v1.PodList{
    51  				ListMeta: metav1.ListMeta{ResourceVersion: "2"},
    52  				Items:    []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2")},
    53  			},
    54  			requestOptions:       metav1.ListOptions{TimeoutSeconds: ptr.To(int64(39))},
    55  			retrievedItems:       []runtime.Object{makePod("p1", "1"), makePod("p2", "2")},
    56  			expectedListRequests: 1,
    57  			expectedRequestOptions: []metav1.ListOptions{
    58  				{
    59  					ResourceVersion:      "2",
    60  					ResourceVersionMatch: metav1.ResourceVersionMatchExact,
    61  					TimeoutSeconds:       ptr.To(int64(39)),
    62  				},
    63  			},
    64  		},
    65  
    66  		{
    67  			name:                      "data consistency check works with unstructured data (dynamic client)",
    68  			lastSyncedResourceVersion: "2",
    69  			listResponse: &unstructured.UnstructuredList{
    70  				Object: map[string]interface{}{
    71  					"apiVersion": "vTest",
    72  					"kind":       "rTestList",
    73  				},
    74  				Items: []unstructured.Unstructured{
    75  					*makeUnstructuredObject("vTest", "rTest", "item1"),
    76  					*makeUnstructuredObject("vTest", "rTest", "item2"),
    77  				},
    78  			},
    79  			requestOptions: metav1.ListOptions{TimeoutSeconds: ptr.To(int64(39))},
    80  			retrievedItems: []runtime.Object{
    81  				makeUnstructuredObject("vTest", "rTest", "item1"),
    82  				makeUnstructuredObject("vTest", "rTest", "item2"),
    83  			},
    84  			expectedListRequests: 1,
    85  			expectedRequestOptions: []metav1.ListOptions{
    86  				{
    87  					ResourceVersion:      "2",
    88  					ResourceVersionMatch: metav1.ResourceVersionMatchExact,
    89  					TimeoutSeconds:       ptr.To(int64(39)),
    90  				},
    91  			},
    92  		},
    93  
    94  		{
    95  			name:                      "legacy, the limit is removed from the list options when it wasn't honored by the watch cache",
    96  			lastSyncedResourceVersion: "2",
    97  			listResponse: &v1.PodList{
    98  				ListMeta: metav1.ListMeta{ResourceVersion: "2"},
    99  				Items:    []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2"), *makePod("p3", "3")},
   100  			},
   101  			requestOptions:       metav1.ListOptions{ResourceVersion: "0", Limit: 2},
   102  			retrievedItems:       []runtime.Object{makePod("p1", "1"), makePod("p2", "2"), makePod("p3", "3")},
   103  			expectedListRequests: 1,
   104  			expectedRequestOptions: []metav1.ListOptions{
   105  				{
   106  					ResourceVersion:      "2",
   107  					ResourceVersionMatch: metav1.ResourceVersionMatchExact,
   108  				},
   109  			},
   110  		},
   111  
   112  		{
   113  			name:                      "the limit is NOT removed from the list options for non-legacy request",
   114  			lastSyncedResourceVersion: "2",
   115  			listResponse: &v1.PodList{
   116  				ListMeta: metav1.ListMeta{ResourceVersion: "2"},
   117  				Items:    []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2"), *makePod("p3", "3")},
   118  			},
   119  			requestOptions:       metav1.ListOptions{ResourceVersion: "2", Limit: 2},
   120  			retrievedItems:       []runtime.Object{makePod("p1", "1"), makePod("p2", "2"), makePod("p3", "3")},
   121  			expectedListRequests: 1,
   122  			expectedRequestOptions: []metav1.ListOptions{
   123  				{
   124  					Limit:                2,
   125  					ResourceVersion:      "2",
   126  					ResourceVersionMatch: metav1.ResourceVersionMatchExact,
   127  				},
   128  			},
   129  		},
   130  
   131  		{
   132  			name:                      "legacy, the limit is NOT removed from the list options when the watch cache is disabled",
   133  			lastSyncedResourceVersion: "2",
   134  			listResponse: &v1.PodList{
   135  				ListMeta: metav1.ListMeta{ResourceVersion: "2"},
   136  				Items:    []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2"), *makePod("p3", "3")},
   137  			},
   138  			requestOptions:       metav1.ListOptions{ResourceVersion: "0", Limit: 5},
   139  			retrievedItems:       []runtime.Object{makePod("p1", "1"), makePod("p2", "2"), makePod("p3", "3")},
   140  			expectedListRequests: 1,
   141  			expectedRequestOptions: []metav1.ListOptions{
   142  				{
   143  					Limit:                5,
   144  					ResourceVersion:      "2",
   145  					ResourceVersionMatch: metav1.ResourceVersionMatchExact,
   146  				},
   147  			},
   148  		},
   149  
   150  		{
   151  			name:                      "data consistency check won't panic when there is no data",
   152  			lastSyncedResourceVersion: "2",
   153  			listResponse: &v1.PodList{
   154  				ListMeta: metav1.ListMeta{ResourceVersion: "2"},
   155  			},
   156  			requestOptions:       metav1.ListOptions{TimeoutSeconds: ptr.To(int64(39))},
   157  			expectedListRequests: 1,
   158  			expectedRequestOptions: []metav1.ListOptions{
   159  				{
   160  					ResourceVersion:      "2",
   161  					ResourceVersionMatch: metav1.ResourceVersionMatchExact,
   162  					TimeoutSeconds:       ptr.To(int64(39)),
   163  				},
   164  			},
   165  		},
   166  
   167  		{
   168  			name:                 "data consistency check won't be performed when Continuation was set",
   169  			requestOptions:       metav1.ListOptions{Continue: "fake continuation token"},
   170  			expectedListRequests: 0,
   171  		},
   172  
   173  		{
   174  			name:                      "data consistency check won't be performed when ResourceVersion was set to 0",
   175  			lastSyncedResourceVersion: "0",
   176  			listResponse: &v1.PodList{
   177  				ListMeta: metav1.ListMeta{ResourceVersion: "0"},
   178  				Items:    []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2")},
   179  			},
   180  			requestOptions:       metav1.ListOptions{},
   181  			expectedListRequests: 0,
   182  		},
   183  
   184  		{
   185  			name:                      "data consistency panics when data is inconsistent",
   186  			lastSyncedResourceVersion: "2",
   187  			listResponse: &v1.PodList{
   188  				ListMeta: metav1.ListMeta{ResourceVersion: "2"},
   189  				Items:    []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2"), *makePod("p3", "3")},
   190  			},
   191  			requestOptions:       metav1.ListOptions{TimeoutSeconds: ptr.To(int64(39))},
   192  			retrievedItems:       []runtime.Object{makePod("p1", "1"), makePod("p2", "2")},
   193  			expectedListRequests: 1,
   194  			expectedRequestOptions: []metav1.ListOptions{
   195  				{
   196  					ResourceVersion:      "2",
   197  					ResourceVersionMatch: metav1.ResourceVersionMatchExact,
   198  					TimeoutSeconds:       ptr.To(int64(39)),
   199  				},
   200  			},
   201  			expectPanic: true,
   202  		},
   203  	}
   204  
   205  	for _, scenario := range scenarios {
   206  		t.Run(scenario.name, func(t *testing.T) {
   207  			ctx := context.TODO()
   208  			if scenario.listResponse == nil {
   209  				scenario.listResponse = &v1.PodList{}
   210  			}
   211  			fakeLister := &listWrapper{response: scenario.listResponse}
   212  			retrievedItemsFunc := func() []runtime.Object {
   213  				return scenario.retrievedItems
   214  			}
   215  
   216  			if scenario.expectPanic {
   217  				require.Panics(t, func() {
   218  					CheckDataConsistency(ctx, "", scenario.lastSyncedResourceVersion, fakeLister.List, scenario.requestOptions, retrievedItemsFunc)
   219  				})
   220  			} else {
   221  				CheckDataConsistency(ctx, "", scenario.lastSyncedResourceVersion, fakeLister.List, scenario.requestOptions, retrievedItemsFunc)
   222  			}
   223  
   224  			require.Equal(t, fakeLister.counter, scenario.expectedListRequests)
   225  			require.Equal(t, fakeLister.requestOptions, scenario.expectedRequestOptions)
   226  		})
   227  	}
   228  }
   229  
   230  func TestDataConsistencyCheckerRetry(t *testing.T) {
   231  	ctx := context.TODO()
   232  	retrievedItemsFunc := func() []*v1.Pod {
   233  		return nil
   234  	}
   235  	stopListErrorAfter := 5
   236  	fakeErrLister := &errorLister{stopErrorAfter: stopListErrorAfter}
   237  
   238  	CheckDataConsistency(ctx, "", "", fakeErrLister.List, metav1.ListOptions{}, retrievedItemsFunc)
   239  	require.Equal(t, fakeErrLister.listCounter, fakeErrLister.stopErrorAfter)
   240  }
   241  
   242  type errorLister struct {
   243  	listCounter    int
   244  	stopErrorAfter int
   245  }
   246  
   247  func (lw *errorLister) List(_ context.Context, _ metav1.ListOptions) (runtime.Object, error) {
   248  	lw.listCounter++
   249  	if lw.listCounter == lw.stopErrorAfter {
   250  		return &v1.PodList{}, nil
   251  	}
   252  	return nil, fmt.Errorf("nasty error")
   253  }
   254  
   255  type listWrapper struct {
   256  	counter        int
   257  	requestOptions []metav1.ListOptions
   258  	response       runtime.Object
   259  }
   260  
   261  func (lw *listWrapper) List(_ context.Context, opts metav1.ListOptions) (runtime.Object, error) {
   262  	lw.counter++
   263  	lw.requestOptions = append(lw.requestOptions, opts)
   264  	return lw.response, nil
   265  }
   266  
   267  func makePod(name, rv string) *v1.Pod {
   268  	return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: name, ResourceVersion: rv, UID: types.UID(name)}}
   269  }
   270  
   271  func makeUnstructuredObject(version, kind, name string) *unstructured.Unstructured {
   272  	return &unstructured.Unstructured{
   273  		Object: map[string]interface{}{
   274  			"apiVersion": version,
   275  			"kind":       kind,
   276  			"metadata": map[string]interface{}{
   277  				"name": name,
   278  			},
   279  		},
   280  	}
   281  }