k8s.io/client-go@v0.31.1/dynamic/fake/simple_test.go (about) 1 /* 2 Copyright 2018 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 fake 18 19 import ( 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 ) 32 33 const ( 34 testGroup = "testgroup" 35 testVersion = "testversion" 36 testResource = "testkinds" 37 testNamespace = "testns" 38 testName = "testname" 39 testKind = "TestKind" 40 testAPIVersion = "testgroup/testversion" 41 ) 42 43 func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured { 44 return &unstructured.Unstructured{ 45 Object: map[string]interface{}{ 46 "apiVersion": apiVersion, 47 "kind": kind, 48 "metadata": map[string]interface{}{ 49 "namespace": namespace, 50 "name": name, 51 }, 52 }, 53 } 54 } 55 56 func newUnstructuredWithSpec(spec map[string]interface{}) *unstructured.Unstructured { 57 u := newUnstructured(testAPIVersion, testKind, testNamespace, testName) 58 u.Object["spec"] = spec 59 return u 60 } 61 62 func TestGet(t *testing.T) { 63 scheme := runtime.NewScheme() 64 65 client := NewSimpleDynamicClient(scheme, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")) 66 get, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).Namespace("ns-foo").Get(context.TODO(), "name-foo", metav1.GetOptions{}) 67 if err != nil { 68 t.Fatal(err) 69 } 70 71 expected := &unstructured.Unstructured{ 72 Object: map[string]interface{}{ 73 "apiVersion": "group/version", 74 "kind": "TheKind", 75 "metadata": map[string]interface{}{ 76 "name": "name-foo", 77 "namespace": "ns-foo", 78 }, 79 }, 80 } 81 if !equality.Semantic.DeepEqual(get, expected) { 82 t.Fatal(cmp.Diff(expected, get)) 83 } 84 } 85 86 func TestListDecoding(t *testing.T) { 87 // this the duplication of logic from the real List API. This will prove that our dynamic client actually returns the gvk 88 uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKindList", "items":[]}`)) 89 if err != nil { 90 t.Fatal(err) 91 } 92 list := uncastObj.(*unstructured.UnstructuredList) 93 expectedList := &unstructured.UnstructuredList{ 94 Object: map[string]interface{}{ 95 "apiVersion": "group/version", 96 "kind": "TheKindList", 97 }, 98 Items: []unstructured.Unstructured{}, 99 } 100 if !equality.Semantic.DeepEqual(list, expectedList) { 101 t.Fatal(cmp.Diff(expectedList, list)) 102 } 103 } 104 105 func TestGetDecoding(t *testing.T) { 106 // this the duplication of logic from the real Get API. This will prove that our dynamic client actually returns the gvk 107 uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKind"}`)) 108 if err != nil { 109 t.Fatal(err) 110 } 111 get := uncastObj.(*unstructured.Unstructured) 112 expectedObj := &unstructured.Unstructured{ 113 Object: map[string]interface{}{ 114 "apiVersion": "group/version", 115 "kind": "TheKind", 116 }, 117 } 118 if !equality.Semantic.DeepEqual(get, expectedObj) { 119 t.Fatal(cmp.Diff(expectedObj, get)) 120 } 121 } 122 123 func TestList(t *testing.T) { 124 scheme := runtime.NewScheme() 125 126 client := NewSimpleDynamicClientWithCustomListKinds(scheme, 127 map[schema.GroupVersionResource]string{ 128 {Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList", 129 }, 130 newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), 131 newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"), 132 newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"), 133 newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"), 134 newUnstructured("group2/version", "TheKind", "ns-foo", "name2-baz"), 135 ) 136 listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(context.TODO(), metav1.ListOptions{}) 137 if err != nil { 138 t.Fatal(err) 139 } 140 141 expected := []unstructured.Unstructured{ 142 *newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"), 143 *newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"), 144 *newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), 145 } 146 if !equality.Semantic.DeepEqual(listFirst.Items, expected) { 147 t.Fatal(cmp.Diff(expected, listFirst.Items)) 148 } 149 } 150 151 func Test_ListKind(t *testing.T) { 152 scheme := runtime.NewScheme() 153 154 client := NewSimpleDynamicClientWithCustomListKinds(scheme, 155 map[schema.GroupVersionResource]string{ 156 {Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList", 157 }, 158 &unstructured.UnstructuredList{ 159 Object: map[string]interface{}{ 160 "apiVersion": "group/version", 161 "kind": "TheKindList", 162 }, 163 Items: []unstructured.Unstructured{ 164 *newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), 165 *newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"), 166 *newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"), 167 }, 168 }, 169 ) 170 listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(context.TODO(), metav1.ListOptions{}) 171 if err != nil { 172 t.Fatal(err) 173 } 174 175 expectedList := &unstructured.UnstructuredList{ 176 Object: map[string]interface{}{ 177 "apiVersion": "group/version", 178 "kind": "TheKindList", 179 "metadata": map[string]interface{}{ 180 "continue": "", 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(cmp.Diff(expectedList, listFirst)) 192 } 193 } 194 195 type 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 204 func (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. 222 func (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 239 func (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", cmp.Diff(tc.expectedPatchedObject, result)) 245 } 246 return nil 247 } 248 249 func 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 310 func 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.SetContinue("") 337 expectedList.Items = append(expectedList.Items, u) 338 339 if diff := cmp.Diff(expectedList, list); diff != "" { 340 t.Fatal("unexpected diff (-want, +got): ", diff) 341 } 342 } 343 344 func TestListWithNoFixturesAndTypedScheme(t *testing.T) { 345 gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource} 346 gvk := gvr.GroupVersion().WithKind(testKind) 347 348 listGVK := gvk 349 listGVK.Kind += "List" 350 351 typedScheme := runtime.NewScheme() 352 typedScheme.AddKnownTypeWithName(gvk, &mockResource{}) 353 typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{}) 354 355 client := NewSimpleDynamicClient(typedScheme) 356 list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{}) 357 358 if err != nil { 359 t.Error("error listing", err) 360 } 361 362 expectedList := &unstructured.UnstructuredList{} 363 expectedList.SetGroupVersionKind(listGVK) 364 expectedList.SetResourceVersion("") // by product of the fake setting resource version 365 expectedList.SetContinue("") 366 367 if diff := cmp.Diff(expectedList, list); diff != "" { 368 t.Fatal("unexpected diff (-want, +got): ", diff) 369 } 370 } 371 372 // This test ensures list works when the dynamic client is seeded with an empty scheme and 373 // unstructured typed fixtures 374 func TestListWithNoScheme(t *testing.T) { 375 gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource} 376 gvk := gvr.GroupVersion().WithKind(testKind) 377 378 listGVK := gvk 379 listGVK.Kind += "List" 380 381 u := unstructured.Unstructured{} 382 u.SetGroupVersionKind(gvk) 383 u.SetName("name") 384 u.SetNamespace("namespace") 385 386 emptyScheme := runtime.NewScheme() 387 388 client := NewSimpleDynamicClient(emptyScheme, &u) 389 list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{}) 390 391 if err != nil { 392 t.Error("error listing", err) 393 } 394 395 expectedList := &unstructured.UnstructuredList{} 396 expectedList.SetGroupVersionKind(listGVK) 397 expectedList.SetResourceVersion("") // by product of the fake setting resource version 398 expectedList.SetContinue("") 399 expectedList.Items = append(expectedList.Items, u) 400 401 if diff := cmp.Diff(expectedList, list); diff != "" { 402 t.Fatal("unexpected diff (-want, +got): ", diff) 403 } 404 } 405 406 // This test ensures list works when the dynamic client is seeded with an empty scheme and 407 // unstructured typed fixtures 408 func TestListWithTypedFixtures(t *testing.T) { 409 gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource} 410 gvk := gvr.GroupVersion().WithKind(testKind) 411 412 listGVK := gvk 413 listGVK.Kind += "List" 414 415 r := mockResource{} 416 r.SetGroupVersionKind(gvk) 417 r.SetName("name") 418 r.SetNamespace("namespace") 419 420 u := unstructured.Unstructured{} 421 u.SetGroupVersionKind(r.GetObjectKind().GroupVersionKind()) 422 u.SetName(r.GetName()) 423 u.SetNamespace(r.GetNamespace()) 424 // Needed see: https://github.com/kubernetes/kubernetes/issues/67610 425 unstructured.SetNestedField(u.Object, nil, "metadata", "creationTimestamp") 426 427 typedScheme := runtime.NewScheme() 428 typedScheme.AddKnownTypeWithName(gvk, &mockResource{}) 429 typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{}) 430 431 client := NewSimpleDynamicClient(typedScheme, &r) 432 list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{}) 433 434 if err != nil { 435 t.Error("error listing", err) 436 } 437 438 expectedList := &unstructured.UnstructuredList{} 439 expectedList.SetGroupVersionKind(listGVK) 440 expectedList.SetResourceVersion("") // by product of the fake setting resource version 441 expectedList.SetContinue("") 442 expectedList.Items = []unstructured.Unstructured{u} 443 444 if diff := cmp.Diff(expectedList, list); diff != "" { 445 t.Fatal("unexpected diff (-want, +got): ", diff) 446 } 447 } 448 449 type ( 450 mockResource struct { 451 metav1.TypeMeta `json:",inline"` 452 metav1.ObjectMeta `json:"metadata"` 453 } 454 mockResourceList struct { 455 metav1.TypeMeta `json:",inline"` 456 metav1.ListMeta `json:"metadata"` 457 458 Items []mockResource 459 } 460 ) 461 462 func (l *mockResourceList) DeepCopyObject() runtime.Object { 463 o := *l 464 return &o 465 } 466 467 func (r *mockResource) DeepCopyObject() runtime.Object { 468 o := *r 469 return &o 470 } 471 472 var _ runtime.Object = (*mockResource)(nil) 473 var _ runtime.Object = (*mockResourceList)(nil)