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