1
2
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
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
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
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
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
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
324
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
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
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
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
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