...

Source file src/sigs.k8s.io/cli-utils/pkg/kstatus/polling/clusterreader/caching_reader_test.go

Documentation: sigs.k8s.io/cli-utils/pkg/kstatus/polling/clusterreader

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package clusterreader
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"sort"
    11  	"strconv"
    12  	"testing"
    13  
    14  	"github.com/google/go-cmp/cmp"
    15  	"github.com/google/go-cmp/cmp/cmpopts"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  	appsv1 "k8s.io/api/apps/v1"
    19  	v1 "k8s.io/api/core/v1"
    20  	"k8s.io/apimachinery/pkg/api/errors"
    21  	"k8s.io/apimachinery/pkg/api/meta"
    22  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    23  	"k8s.io/apimachinery/pkg/runtime/schema"
    24  	"sigs.k8s.io/cli-utils/pkg/object"
    25  	"sigs.k8s.io/cli-utils/pkg/testutil"
    26  	"sigs.k8s.io/controller-runtime/pkg/client"
    27  )
    28  
    29  var (
    30  	deploymentGVK = appsv1.SchemeGroupVersion.WithKind("Deployment")
    31  	rsGVK         = appsv1.SchemeGroupVersion.WithKind("ReplicaSet")
    32  	podGVK        = v1.SchemeGroupVersion.WithKind("Pod")
    33  	crdGVK        = schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"}
    34  )
    35  
    36  func TestSync(t *testing.T) {
    37  	// Use a custom Asserter to customize the comparison options
    38  	asserter := testutil.NewAsserter(
    39  		cmpopts.EquateErrors(),
    40  		gkNamespaceComparer(),
    41  		cacheEntryComparer(),
    42  	)
    43  
    44  	testCases := map[string]struct {
    45  		identifiers    object.ObjMetadataSet
    46  		clusterObjs    map[gkNamespace][]unstructured.Unstructured
    47  		expectedSynced []gkNamespace
    48  		expectedCached map[gkNamespace]cacheEntry
    49  	}{
    50  		"no identifiers": {
    51  			identifiers:    object.ObjMetadataSet{},
    52  			expectedCached: map[gkNamespace]cacheEntry{},
    53  		},
    54  		"same GVK in multiple namespaces": {
    55  			identifiers: object.ObjMetadataSet{
    56  				{
    57  					GroupKind: deploymentGVK.GroupKind(),
    58  					Name:      "deployment",
    59  					Namespace: "Foo",
    60  				},
    61  				{
    62  					GroupKind: deploymentGVK.GroupKind(),
    63  					Name:      "deployment",
    64  					Namespace: "Bar",
    65  				},
    66  			},
    67  			clusterObjs: map[gkNamespace][]unstructured.Unstructured{
    68  				{GroupKind: deploymentGVK.GroupKind(), Namespace: "Foo"}: {
    69  					{
    70  						Object: map[string]interface{}{
    71  							"apiVersion": "apps/v1",
    72  							"kind":       "Deployment",
    73  							"metadata": map[string]interface{}{
    74  								"name":      "deployment-1",
    75  								"namespace": "Foo",
    76  							},
    77  						},
    78  					},
    79  				},
    80  				{GroupKind: deploymentGVK.GroupKind(), Namespace: "Bar"}: {
    81  					{
    82  						Object: map[string]interface{}{
    83  							"apiVersion": "apps/v1",
    84  							"kind":       "Deployment",
    85  							"metadata": map[string]interface{}{
    86  								"name":      "deployment-2",
    87  								"namespace": "Bar",
    88  							},
    89  						},
    90  					},
    91  				},
    92  			},
    93  			expectedSynced: []gkNamespace{
    94  				{GroupKind: deploymentGVK.GroupKind(), Namespace: "Foo"},
    95  				{GroupKind: rsGVK.GroupKind(), Namespace: "Foo"},
    96  				{GroupKind: podGVK.GroupKind(), Namespace: "Foo"},
    97  				{GroupKind: deploymentGVK.GroupKind(), Namespace: "Bar"},
    98  				{GroupKind: rsGVK.GroupKind(), Namespace: "Bar"},
    99  				{GroupKind: podGVK.GroupKind(), Namespace: "Bar"},
   100  			},
   101  			expectedCached: map[gkNamespace]cacheEntry{
   102  				{GroupKind: deploymentGVK.GroupKind(), Namespace: "Foo"}: {
   103  					resources: unstructured.UnstructuredList{
   104  						Object: map[string]interface{}{"apiVersion": "apps/v1", "kind": "Deployment"},
   105  						Items: []unstructured.Unstructured{
   106  							{
   107  								Object: map[string]interface{}{
   108  									"apiVersion": "apps/v1",
   109  									"kind":       "Deployment",
   110  									"metadata": map[string]interface{}{
   111  										"name":      "deployment-1",
   112  										"namespace": "Foo",
   113  									},
   114  								},
   115  							},
   116  						},
   117  					},
   118  				},
   119  				{GroupKind: rsGVK.GroupKind(), Namespace: "Foo"}: {
   120  					resources: unstructured.UnstructuredList{
   121  						Object: map[string]interface{}{"apiVersion": "apps/v1", "kind": "ReplicaSet"},
   122  					},
   123  				},
   124  				{GroupKind: podGVK.GroupKind(), Namespace: "Foo"}: {
   125  					resources: unstructured.UnstructuredList{
   126  						Object: map[string]interface{}{"apiVersion": "v1", "kind": "Pod"},
   127  					},
   128  				},
   129  				{GroupKind: deploymentGVK.GroupKind(), Namespace: "Bar"}: {
   130  					resources: unstructured.UnstructuredList{
   131  						Object: map[string]interface{}{"apiVersion": "apps/v1", "kind": "Deployment"},
   132  						Items: []unstructured.Unstructured{
   133  							{
   134  								Object: map[string]interface{}{
   135  									"apiVersion": "apps/v1",
   136  									"kind":       "Deployment",
   137  									"metadata": map[string]interface{}{
   138  										"name":      "deployment-2",
   139  										"namespace": "Bar",
   140  									},
   141  								},
   142  							},
   143  						},
   144  					},
   145  				},
   146  				{GroupKind: rsGVK.GroupKind(), Namespace: "Bar"}: {
   147  					resources: unstructured.UnstructuredList{
   148  						Object: map[string]interface{}{"apiVersion": "apps/v1", "kind": "ReplicaSet"},
   149  					},
   150  				},
   151  				{GroupKind: podGVK.GroupKind(), Namespace: "Bar"}: {
   152  					resources: unstructured.UnstructuredList{
   153  						Object: map[string]interface{}{"apiVersion": "v1", "kind": "Pod"},
   154  					},
   155  				},
   156  			},
   157  		},
   158  	}
   159  
   160  	barPodGKN := gkNamespace{GroupKind: podGVK.GroupKind(), Namespace: "Bar"}
   161  	// 1001 = 3 pages of 500
   162  	barObjs := make([]unstructured.Unstructured, 1001)
   163  	for i := 0; i < len(barObjs); i++ {
   164  		barObjs[i] = unstructured.Unstructured{
   165  			Object: map[string]interface{}{
   166  				"apiVersion": podGVK.GroupVersion().String(),
   167  				"kind":       podGVK.Kind,
   168  				"metadata": map[string]interface{}{
   169  					"name":      fmt.Sprintf("pod-%d", i),
   170  					"namespace": barPodGKN.Namespace,
   171  				},
   172  			},
   173  		}
   174  	}
   175  	testCases["paginated"] = struct {
   176  		identifiers    object.ObjMetadataSet
   177  		clusterObjs    map[gkNamespace][]unstructured.Unstructured
   178  		expectedSynced []gkNamespace
   179  		expectedCached map[gkNamespace]cacheEntry
   180  	}{
   181  		identifiers: object.ObjMetadataSet{
   182  			// any one pod
   183  			{
   184  				GroupKind: podGVK.GroupKind(),
   185  				Name:      "pod-99",
   186  				Namespace: barPodGKN.Namespace,
   187  			},
   188  		},
   189  		clusterObjs: map[gkNamespace][]unstructured.Unstructured{
   190  			barPodGKN: barObjs,
   191  		},
   192  		expectedSynced: []gkNamespace{
   193  			// expect 3 paginated calls to LIST
   194  			barPodGKN,
   195  			barPodGKN,
   196  			barPodGKN,
   197  		},
   198  		expectedCached: map[gkNamespace]cacheEntry{
   199  			barPodGKN: {
   200  				resources: unstructured.UnstructuredList{
   201  					Object: map[string]interface{}{
   202  						"apiVersion": podGVK.GroupVersion().String(),
   203  						"kind":       podGVK.Kind,
   204  					},
   205  					// all the deployments in the same namespace
   206  					Items: barObjs,
   207  				},
   208  			},
   209  		},
   210  	}
   211  
   212  	fakeMapper := testutil.NewFakeRESTMapper(
   213  		deploymentGVK,
   214  		rsGVK,
   215  		v1.SchemeGroupVersion.WithKind("Pod"),
   216  	)
   217  
   218  	for tn, tc := range testCases {
   219  		t.Run(tn, func(t *testing.T) {
   220  			fakeReader := &fakeReader{
   221  				clusterObjs: tc.clusterObjs,
   222  			}
   223  
   224  			clusterReader, err := newCachingClusterReader(fakeReader, fakeMapper, tc.identifiers)
   225  			require.NoError(t, err)
   226  
   227  			err = clusterReader.Sync(context.Background())
   228  			require.NoError(t, err)
   229  
   230  			synced := fakeReader.syncedGVKNamespaces
   231  			sortGVKNamespaces(synced)
   232  			expectedSynced := tc.expectedSynced
   233  			sortGVKNamespaces(expectedSynced)
   234  			asserter.Equal(t, expectedSynced, synced)
   235  			asserter.Equal(t, tc.expectedCached, clusterReader.cache)
   236  		})
   237  	}
   238  }
   239  
   240  func TestSync_Errors(t *testing.T) {
   241  	testCases := map[string]struct {
   242  		mapper          meta.RESTMapper
   243  		readerError     error
   244  		expectSyncError bool
   245  		cacheError      bool
   246  		cacheErrorText  string
   247  	}{
   248  		"mapping and reader are successful": {
   249  			mapper: testutil.NewFakeRESTMapper(
   250  				crdGVK,
   251  			),
   252  			readerError:     nil,
   253  			expectSyncError: false,
   254  			cacheError:      false,
   255  		},
   256  		"reader returns NotFound error": {
   257  			mapper: testutil.NewFakeRESTMapper(
   258  				crdGVK,
   259  			),
   260  			readerError: errors.NewNotFound(schema.GroupResource{
   261  				Group:    "apiextensions.k8s.io",
   262  				Resource: "customresourcedefinitions",
   263  			}, "my-crd"),
   264  			expectSyncError: false,
   265  			cacheError:      true,
   266  			cacheErrorText:  `customresourcedefinitions.apiextensions.k8s.io "my-crd" not found`,
   267  		},
   268  		"reader returns other error": {
   269  			mapper: testutil.NewFakeRESTMapper(
   270  				crdGVK,
   271  			),
   272  			readerError:     errors.NewInternalError(fmt.Errorf("testing")),
   273  			expectSyncError: false,
   274  			cacheError:      true,
   275  			cacheErrorText:  "Internal error occurred: testing",
   276  		},
   277  		"mapping not found": {
   278  			mapper:          testutil.NewFakeRESTMapper(),
   279  			expectSyncError: false,
   280  			cacheError:      true,
   281  			cacheErrorText:  `no matches for kind "CustomResourceDefinition" in group "apiextensions.k8s.io"`,
   282  		},
   283  	}
   284  
   285  	for tn, tc := range testCases {
   286  		t.Run(tn, func(t *testing.T) {
   287  			identifiers := object.ObjMetadataSet{
   288  				{
   289  					Name: "my-crd",
   290  					GroupKind: schema.GroupKind{
   291  						Group: "apiextensions.k8s.io",
   292  						Kind:  "CustomResourceDefinition",
   293  					},
   294  				},
   295  			}
   296  
   297  			fakeReader := &fakeReader{
   298  				err: tc.readerError,
   299  			}
   300  
   301  			clusterReader, err := newCachingClusterReader(fakeReader, tc.mapper, identifiers)
   302  			require.NoError(t, err)
   303  
   304  			err = clusterReader.Sync(context.Background())
   305  
   306  			if tc.expectSyncError {
   307  				assert.Equal(t, tc.readerError, err)
   308  				return
   309  			}
   310  			require.NoError(t, err)
   311  
   312  			cacheEntry, found := clusterReader.cache[gkNamespace{
   313  				GroupKind: crdGVK.GroupKind(),
   314  			}]
   315  			require.True(t, found)
   316  			if tc.cacheError {
   317  				assert.EqualError(t, cacheEntry.err, tc.cacheErrorText)
   318  			}
   319  		})
   320  	}
   321  }
   322  
   323  // newCachingClusterReader creates a new CachingClusterReader and returns it as the concrete
   324  // type instead of engine.ClusterReader.
   325  func newCachingClusterReader(reader client.Reader, mapper meta.RESTMapper, identifiers object.ObjMetadataSet) (*CachingClusterReader, error) {
   326  	r, err := NewCachingClusterReader(reader, mapper, identifiers)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  	return r.(*CachingClusterReader), nil
   331  }
   332  
   333  func sortGVKNamespaces(gvkNamespaces []gkNamespace) {
   334  	sort.Slice(gvkNamespaces, func(i, j int) bool {
   335  		if gvkNamespaces[i].GroupKind.String() != gvkNamespaces[j].GroupKind.String() {
   336  			return gvkNamespaces[i].GroupKind.String() < gvkNamespaces[j].GroupKind.String()
   337  		}
   338  		return gvkNamespaces[i].Namespace < gvkNamespaces[j].Namespace
   339  	})
   340  }
   341  
   342  type fakeReader struct {
   343  	clusterObjs         map[gkNamespace][]unstructured.Unstructured
   344  	syncedGVKNamespaces []gkNamespace
   345  	err                 error
   346  }
   347  
   348  func (f *fakeReader) Get(_ context.Context, _ client.ObjectKey, _ client.Object, opts ...client.GetOption) error {
   349  	return nil
   350  }
   351  
   352  //nolint:gocritic
   353  func (f *fakeReader) List(_ context.Context, list client.ObjectList, opts ...client.ListOption) error {
   354  	listOpts := &client.ListOptions{}
   355  	listOpts.ApplyOptions(opts)
   356  
   357  	gvk := list.GetObjectKind().GroupVersionKind()
   358  	query := gkNamespace{
   359  		GroupKind: gvk.GroupKind(),
   360  		Namespace: listOpts.Namespace,
   361  	}
   362  
   363  	f.syncedGVKNamespaces = append(f.syncedGVKNamespaces, query)
   364  
   365  	if f.err != nil {
   366  		return f.err
   367  	}
   368  
   369  	results, ok := f.clusterObjs[query]
   370  	if !ok {
   371  		// no results
   372  		return nil
   373  	}
   374  
   375  	uList, ok := list.(*unstructured.UnstructuredList)
   376  	if !ok {
   377  		return fmt.Errorf("unexpected list type: %T", list)
   378  	}
   379  
   380  	if listOpts.Limit > 0 && len(results) > 0 {
   381  		// return paginated results from Continue to Continue + Limit
   382  		start := int64(0)
   383  		if listOpts.Continue != "" {
   384  			var err error
   385  			start, err = strconv.ParseInt(listOpts.Continue, 10, 64)
   386  			if err != nil {
   387  				return fmt.Errorf("invalid continue value: %q", listOpts.Continue)
   388  			}
   389  		}
   390  		end := start + listOpts.Limit
   391  		max := int64(len(results))
   392  		if end > max {
   393  			end = max
   394  		} else {
   395  			// set continue if more results are available
   396  			uList.SetContinue(strconv.FormatInt(end, 10))
   397  		}
   398  		uList.Items = append(uList.Items, results[start:end]...)
   399  	} else {
   400  		uList.Items = results
   401  	}
   402  
   403  	return nil
   404  }
   405  
   406  func gkNamespaceComparer() cmp.Option {
   407  	return cmp.Comparer(func(x, y gkNamespace) bool {
   408  		return x.GroupKind == y.GroupKind &&
   409  			x.Namespace == y.Namespace
   410  	})
   411  }
   412  
   413  func cacheEntryComparer() cmp.Option {
   414  	return cmp.Comparer(func(x, y cacheEntry) bool {
   415  		if x.err != y.err {
   416  			return false
   417  		}
   418  		xBytes, err := json.Marshal(x.resources)
   419  		if err != nil {
   420  			panic(fmt.Sprintf("failed to marshal item x to json: %v", err))
   421  		}
   422  		yBytes, err := json.Marshal(y.resources)
   423  		if err != nil {
   424  			panic(fmt.Sprintf("failed to marshal item y to json: %v", err))
   425  		}
   426  		return string(xBytes) == string(yBytes)
   427  	})
   428  }
   429  

View as plain text