1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package fake
18
19import (
20	"context"
21	"fmt"
22	"testing"
23
24	"github.com/google/go-cmp/cmp"
25	"k8s.io/apimachinery/pkg/api/equality"
26	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28	"k8s.io/apimachinery/pkg/runtime"
29	"k8s.io/apimachinery/pkg/runtime/schema"
30	"k8s.io/apimachinery/pkg/types"
31	"k8s.io/apimachinery/pkg/util/diff"
32)
33
34const (
35	testGroup      = "testgroup"
36	testVersion    = "testversion"
37	testResource   = "testkinds"
38	testNamespace  = "testns"
39	testName       = "testname"
40	testKind       = "TestKind"
41	testAPIVersion = "testgroup/testversion"
42)
43
44func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
45	return &unstructured.Unstructured{
46		Object: map[string]interface{}{
47			"apiVersion": apiVersion,
48			"kind":       kind,
49			"metadata": map[string]interface{}{
50				"namespace": namespace,
51				"name":      name,
52			},
53		},
54	}
55}
56
57func newUnstructuredWithSpec(spec map[string]interface{}) *unstructured.Unstructured {
58	u := newUnstructured(testAPIVersion, testKind, testNamespace, testName)
59	u.Object["spec"] = spec
60	return u
61}
62
63func TestGet(t *testing.T) {
64	scheme := runtime.NewScheme()
65
66	client := NewSimpleDynamicClient(scheme, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"))
67	get, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).Namespace("ns-foo").Get(context.TODO(), "name-foo", metav1.GetOptions{})
68	if err != nil {
69		t.Fatal(err)
70	}
71
72	expected := &unstructured.Unstructured{
73		Object: map[string]interface{}{
74			"apiVersion": "group/version",
75			"kind":       "TheKind",
76			"metadata": map[string]interface{}{
77				"name":      "name-foo",
78				"namespace": "ns-foo",
79			},
80		},
81	}
82	if !equality.Semantic.DeepEqual(get, expected) {
83		t.Fatal(diff.ObjectGoPrintDiff(expected, get))
84	}
85}
86
87func TestListDecoding(t *testing.T) {
88	// this the duplication of logic from the real List API.  This will prove that our dynamic client actually returns the gvk
89	uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKindList", "items":[]}`))
90	if err != nil {
91		t.Fatal(err)
92	}
93	list := uncastObj.(*unstructured.UnstructuredList)
94	expectedList := &unstructured.UnstructuredList{
95		Object: map[string]interface{}{
96			"apiVersion": "group/version",
97			"kind":       "TheKindList",
98		},
99		Items: []unstructured.Unstructured{},
100	}
101	if !equality.Semantic.DeepEqual(list, expectedList) {
102		t.Fatal(diff.ObjectGoPrintDiff(expectedList, list))
103	}
104}
105
106func TestGetDecoding(t *testing.T) {
107	// this the duplication of logic from the real Get API.  This will prove that our dynamic client actually returns the gvk
108	uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKind"}`))
109	if err != nil {
110		t.Fatal(err)
111	}
112	get := uncastObj.(*unstructured.Unstructured)
113	expectedObj := &unstructured.Unstructured{
114		Object: map[string]interface{}{
115			"apiVersion": "group/version",
116			"kind":       "TheKind",
117		},
118	}
119	if !equality.Semantic.DeepEqual(get, expectedObj) {
120		t.Fatal(diff.ObjectGoPrintDiff(expectedObj, get))
121	}
122}
123
124func TestList(t *testing.T) {
125	scheme := runtime.NewScheme()
126
127	client := NewSimpleDynamicClientWithCustomListKinds(scheme,
128		map[schema.GroupVersionResource]string{
129			{Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList",
130		},
131		newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
132		newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"),
133		newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
134		newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
135		newUnstructured("group2/version", "TheKind", "ns-foo", "name2-baz"),
136	)
137	listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(context.TODO(), metav1.ListOptions{})
138	if err != nil {
139		t.Fatal(err)
140	}
141
142	expected := []unstructured.Unstructured{
143		*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
144		*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
145		*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
146	}
147	if !equality.Semantic.DeepEqual(listFirst.Items, expected) {
148		t.Fatal(diff.ObjectGoPrintDiff(expected, listFirst.Items))
149	}
150}
151
152func Test_ListKind(t *testing.T) {
153	scheme := runtime.NewScheme()
154
155	client := NewSimpleDynamicClientWithCustomListKinds(scheme,
156		map[schema.GroupVersionResource]string{
157			{Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList",
158		},
159		&unstructured.UnstructuredList{
160			Object: map[string]interface{}{
161				"apiVersion": "group/version",
162				"kind":       "TheKindList",
163			},
164			Items: []unstructured.Unstructured{
165				*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
166				*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
167				*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
168			},
169		},
170	)
171	listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(context.TODO(), metav1.ListOptions{})
172	if err != nil {
173		t.Fatal(err)
174	}
175
176	expectedList := &unstructured.UnstructuredList{
177		Object: map[string]interface{}{
178			"apiVersion": "group/version",
179			"kind":       "TheKindList",
180			"metadata": map[string]interface{}{
181				"resourceVersion": "",
182			},
183		},
184		Items: []unstructured.Unstructured{
185			*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
186			*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
187			*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
188		},
189	}
190	if !equality.Semantic.DeepEqual(listFirst, expectedList) {
191		t.Fatal(diff.ObjectGoPrintDiff(expectedList, listFirst))
192	}
193}
194
195type patchTestCase struct {
196	name                  string
197	object                runtime.Object
198	patchType             types.PatchType
199	patchBytes            []byte
200	wantErrMsg            string
201	expectedPatchedObject runtime.Object
202}
203
204func (tc *patchTestCase) runner(t *testing.T) {
205	client := NewSimpleDynamicClient(runtime.NewScheme(), tc.object)
206	resourceInterface := client.Resource(schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}).Namespace(testNamespace)
207
208	got, recErr := resourceInterface.Patch(context.TODO(), testName, tc.patchType, tc.patchBytes, metav1.PatchOptions{})
209
210	if err := tc.verifyErr(recErr); err != nil {
211		t.Error(err)
212	}
213
214	if err := tc.verifyResult(got); err != nil {
215		t.Error(err)
216	}
217
218}
219
220// verifyErr verifies that the given error returned from Patch is the error
221// expected by the test case.
222func (tc *patchTestCase) verifyErr(err error) error {
223	if tc.wantErrMsg != "" && err == nil {
224		return fmt.Errorf("want error, got nil")
225	}
226
227	if tc.wantErrMsg == "" && err != nil {
228		return fmt.Errorf("want no error, got %v", err)
229	}
230
231	if err != nil {
232		if want, got := tc.wantErrMsg, err.Error(); want != got {
233			return fmt.Errorf("incorrect error: want: %q got: %q", want, got)
234		}
235	}
236	return nil
237}
238
239func (tc *patchTestCase) verifyResult(result *unstructured.Unstructured) error {
240	if tc.expectedPatchedObject == nil && result == nil {
241		return nil
242	}
243	if !equality.Semantic.DeepEqual(result, tc.expectedPatchedObject) {
244		return fmt.Errorf("unexpected diff in received object: %s", diff.ObjectGoPrintDiff(tc.expectedPatchedObject, result))
245	}
246	return nil
247}
248
249func TestPatch(t *testing.T) {
250	testCases := []patchTestCase{
251		{
252			name:       "jsonpatch fails with merge type",
253			object:     newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
254			patchType:  types.StrategicMergePatchType,
255			patchBytes: []byte(`[]`),
256			wantErrMsg: "invalid JSON document",
257		}, {
258			name:      "jsonpatch works with empty patch",
259			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
260			patchType: types.JSONPatchType,
261			// No-op
262			patchBytes:            []byte(`[]`),
263			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
264		}, {
265			name:      "jsonpatch works with simple change patch",
266			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
267			patchType: types.JSONPatchType,
268			// change spec.foo from bar to foobar
269			patchBytes:            []byte(`[{"op": "replace", "path": "/spec/foo", "value": "foobar"}]`),
270			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "foobar"}),
271		}, {
272			name:      "jsonpatch works with simple addition",
273			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
274			patchType: types.JSONPatchType,
275			// add spec.newvalue = dummy
276			patchBytes:            []byte(`[{"op": "add", "path": "/spec/newvalue", "value": "dummy"}]`),
277			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar", "newvalue": "dummy"}),
278		}, {
279			name:      "jsonpatch works with simple deletion",
280			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar", "toremove": "shouldnotbehere"}),
281			patchType: types.JSONPatchType,
282			// remove spec.newvalue = dummy
283			patchBytes:            []byte(`[{"op": "remove", "path": "/spec/toremove"}]`),
284			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
285		}, {
286			name:      "strategic merge patch fails with JSONPatch",
287			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
288			patchType: types.StrategicMergePatchType,
289			// add spec.newvalue = dummy
290			patchBytes: []byte(`[{"op": "add", "path": "/spec/newvalue", "value": "dummy"}]`),
291			wantErrMsg: "invalid JSON document",
292		}, {
293			name:                  "merge patch works with simple replacement",
294			object:                newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
295			patchType:             types.MergePatchType,
296			patchBytes:            []byte(`{ "spec": { "foo": "baz" } }`),
297			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "baz"}),
298		},
299		// TODO: Add tests for strategic merge using v1.Pod for example to ensure the test cases
300		// demonstrate expected use cases.
301	}
302
303	for _, tc := range testCases {
304		t.Run(tc.name, tc.runner)
305	}
306}
307
308// This test ensures list works when the fake dynamic client is seeded with a typed scheme and
309// unstructured type fixtures
310func TestListWithUnstructuredObjectsAndTypedScheme(t *testing.T) {
311	gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}
312	gvk := gvr.GroupVersion().WithKind(testKind)
313
314	listGVK := gvk
315	listGVK.Kind += "List"
316
317	u := unstructured.Unstructured{}
318	u.SetGroupVersionKind(gvk)
319	u.SetName("name")
320	u.SetNamespace("namespace")
321
322	typedScheme := runtime.NewScheme()
323	typedScheme.AddKnownTypeWithName(gvk, &mockResource{})
324	typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{})
325
326	client := NewSimpleDynamicClient(typedScheme, &u)
327	list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{})
328
329	if err != nil {
330		t.Error("error listing", err)
331	}
332
333	expectedList := &unstructured.UnstructuredList{}
334	expectedList.SetGroupVersionKind(listGVK)
335	expectedList.SetResourceVersion("") // by product of the fake setting resource version
336	expectedList.Items = append(expectedList.Items, u)
337
338	if diff := cmp.Diff(expectedList, list); diff != "" {
339		t.Fatal("unexpected diff (-want, +got): ", diff)
340	}
341}
342
343func TestListWithNoFixturesAndTypedScheme(t *testing.T) {
344	gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}
345	gvk := gvr.GroupVersion().WithKind(testKind)
346
347	listGVK := gvk
348	listGVK.Kind += "List"
349
350	typedScheme := runtime.NewScheme()
351	typedScheme.AddKnownTypeWithName(gvk, &mockResource{})
352	typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{})
353
354	client := NewSimpleDynamicClient(typedScheme)
355	list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{})
356
357	if err != nil {
358		t.Error("error listing", err)
359	}
360
361	expectedList := &unstructured.UnstructuredList{}
362	expectedList.SetGroupVersionKind(listGVK)
363	expectedList.SetResourceVersion("") // by product of the fake setting resource version
364
365	if diff := cmp.Diff(expectedList, list); diff != "" {
366		t.Fatal("unexpected diff (-want, +got): ", diff)
367	}
368}
369
370// This test ensures list works when the dynamic client is seeded with an empty scheme and
371// unstructured typed fixtures
372func TestListWithNoScheme(t *testing.T) {
373	gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}
374	gvk := gvr.GroupVersion().WithKind(testKind)
375
376	listGVK := gvk
377	listGVK.Kind += "List"
378
379	u := unstructured.Unstructured{}
380	u.SetGroupVersionKind(gvk)
381	u.SetName("name")
382	u.SetNamespace("namespace")
383
384	emptyScheme := runtime.NewScheme()
385
386	client := NewSimpleDynamicClient(emptyScheme, &u)
387	list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{})
388
389	if err != nil {
390		t.Error("error listing", err)
391	}
392
393	expectedList := &unstructured.UnstructuredList{}
394	expectedList.SetGroupVersionKind(listGVK)
395	expectedList.SetResourceVersion("") // by product of the fake setting resource version
396	expectedList.Items = append(expectedList.Items, u)
397
398	if diff := cmp.Diff(expectedList, list); diff != "" {
399		t.Fatal("unexpected diff (-want, +got): ", diff)
400	}
401}
402
403// This test ensures list works when the dynamic client is seeded with an empty scheme and
404// unstructured typed fixtures
405func TestListWithTypedFixtures(t *testing.T) {
406	gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}
407	gvk := gvr.GroupVersion().WithKind(testKind)
408
409	listGVK := gvk
410	listGVK.Kind += "List"
411
412	r := mockResource{}
413	r.SetGroupVersionKind(gvk)
414	r.SetName("name")
415	r.SetNamespace("namespace")
416
417	u := unstructured.Unstructured{}
418	u.SetGroupVersionKind(r.GetObjectKind().GroupVersionKind())
419	u.SetName(r.GetName())
420	u.SetNamespace(r.GetNamespace())
421	// Needed see: https://github.com/kubernetes/kubernetes/issues/67610
422	unstructured.SetNestedField(u.Object, nil, "metadata", "creationTimestamp")
423
424	typedScheme := runtime.NewScheme()
425	typedScheme.AddKnownTypeWithName(gvk, &mockResource{})
426	typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{})
427
428	client := NewSimpleDynamicClient(typedScheme, &r)
429	list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{})
430
431	if err != nil {
432		t.Error("error listing", err)
433	}
434
435	expectedList := &unstructured.UnstructuredList{}
436	expectedList.SetGroupVersionKind(listGVK)
437	expectedList.SetResourceVersion("") // by product of the fake setting resource version
438	expectedList.Items = []unstructured.Unstructured{u}
439
440	if diff := cmp.Diff(expectedList, list); diff != "" {
441		t.Fatal("unexpected diff (-want, +got): ", diff)
442	}
443}
444
445type (
446	mockResource struct {
447		metav1.TypeMeta   `json:",inline"`
448		metav1.ObjectMeta `json:"metadata"`
449	}
450	mockResourceList struct {
451		metav1.TypeMeta `json:",inline"`
452		metav1.ListMeta `json:"metadata"`
453
454		Items []mockResource
455	}
456)
457
458func (l *mockResourceList) DeepCopyObject() runtime.Object {
459	o := *l
460	return &o
461}
462
463func (r *mockResource) DeepCopyObject() runtime.Object {
464	o := *r
465	return &o
466}
467
468var _ runtime.Object = (*mockResource)(nil)
469var _ runtime.Object = (*mockResourceList)(nil)
470