k8s.io/client-go@v0.31.1/dynamic/client_test.go (about) 1 /* 2 Copyright 2016 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 dynamic 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "io" 24 "net/http" 25 "net/http/httptest" 26 "reflect" 27 "strings" 28 "testing" 29 30 "github.com/google/go-cmp/cmp" 31 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apimachinery/pkg/runtime/schema" 36 "k8s.io/apimachinery/pkg/runtime/serializer/streaming" 37 "k8s.io/apimachinery/pkg/types" 38 "k8s.io/apimachinery/pkg/watch" 39 clientfeatures "k8s.io/client-go/features" 40 clientfeaturestesting "k8s.io/client-go/features/testing" 41 restclient "k8s.io/client-go/rest" 42 restclientwatch "k8s.io/client-go/rest/watch" 43 ) 44 45 func getJSON(version, kind, name string) []byte { 46 return []byte(fmt.Sprintf(`{"apiVersion": %q, "kind": %q, "metadata": {"name": %q}}`, version, kind, name)) 47 } 48 49 func getListJSON(version, kind string, items ...[]byte) []byte { 50 json := fmt.Sprintf(`{"apiVersion": %q, "kind": %q, "items": [%s]}`, 51 version, kind, bytes.Join(items, []byte(","))) 52 return []byte(json) 53 } 54 55 func getObject(version, kind, name string) *unstructured.Unstructured { 56 return &unstructured.Unstructured{ 57 Object: map[string]interface{}{ 58 "apiVersion": version, 59 "kind": kind, 60 "metadata": map[string]interface{}{ 61 "name": name, 62 }, 63 }, 64 } 65 } 66 67 func getClientServer(h func(http.ResponseWriter, *http.Request)) (Interface, *httptest.Server, error) { 68 srv := httptest.NewServer(http.HandlerFunc(h)) 69 cl, err := NewForConfig(&restclient.Config{ 70 Host: srv.URL, 71 }) 72 if err != nil { 73 srv.Close() 74 return nil, nil, err 75 } 76 return cl, srv, nil 77 } 78 79 func TestList(t *testing.T) { 80 tcs := []struct { 81 name string 82 namespace string 83 path string 84 resp []byte 85 want *unstructured.UnstructuredList 86 }{ 87 { 88 name: "normal_list", 89 path: "/apis/gtest/vtest/rtest", 90 resp: getListJSON("vTest", "rTestList", 91 getJSON("vTest", "rTest", "item1"), 92 getJSON("vTest", "rTest", "item2")), 93 want: &unstructured.UnstructuredList{ 94 Object: map[string]interface{}{ 95 "apiVersion": "vTest", 96 "kind": "rTestList", 97 }, 98 Items: []unstructured.Unstructured{ 99 *getObject("vTest", "rTest", "item1"), 100 *getObject("vTest", "rTest", "item2"), 101 }, 102 }, 103 }, 104 { 105 name: "namespaced_list", 106 namespace: "nstest", 107 path: "/apis/gtest/vtest/namespaces/nstest/rtest", 108 resp: getListJSON("vTest", "rTestList", 109 getJSON("vTest", "rTest", "item1"), 110 getJSON("vTest", "rTest", "item2")), 111 want: &unstructured.UnstructuredList{ 112 Object: map[string]interface{}{ 113 "apiVersion": "vTest", 114 "kind": "rTestList", 115 }, 116 Items: []unstructured.Unstructured{ 117 *getObject("vTest", "rTest", "item1"), 118 *getObject("vTest", "rTest", "item2"), 119 }, 120 }, 121 }, 122 } 123 for _, tc := range tcs { 124 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"} 125 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 126 if r.Method != "GET" { 127 t.Errorf("List(%q) got HTTP method %s. wanted GET", tc.name, r.Method) 128 } 129 130 if r.URL.Path != tc.path { 131 t.Errorf("List(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 132 } 133 134 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 135 w.Write(tc.resp) 136 }) 137 if err != nil { 138 t.Errorf("unexpected error when creating client: %v", err) 139 continue 140 } 141 defer srv.Close() 142 143 got, err := cl.Resource(resource).Namespace(tc.namespace).List(context.TODO(), metav1.ListOptions{}) 144 if err != nil { 145 t.Errorf("unexpected error when listing %q: %v", tc.name, err) 146 continue 147 } 148 149 if !reflect.DeepEqual(got, tc.want) { 150 t.Errorf("List(%q) want: %v\ngot: %v", tc.name, tc.want, got) 151 } 152 } 153 } 154 155 func TestWatchList(t *testing.T) { 156 clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, true) 157 158 type requestParam struct { 159 Path string 160 Query string 161 } 162 163 scenarios := []struct { 164 name string 165 namespace string 166 watchResponse []watch.Event 167 listResponse []byte 168 169 expectedRequestParams []requestParam 170 expectedList *unstructured.UnstructuredList 171 }{ 172 { 173 name: "watch-list request for cluster wide resource", 174 watchResponse: []watch.Event{ 175 {Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "item1")}, 176 {Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "item2")}, 177 {Type: watch.Bookmark, Object: func() runtime.Object { 178 obj := getObject("gtest/vTest", "rTest", "item2") 179 obj.SetResourceVersion("10") 180 obj.SetAnnotations(map[string]string{metav1.InitialEventsAnnotationKey: "true"}) 181 return obj 182 }()}, 183 }, 184 expectedRequestParams: []requestParam{ 185 { 186 Path: "/apis/gtest/vtest/rtest", 187 Query: "allowWatchBookmarks=true&resourceVersionMatch=NotOlderThan&sendInitialEvents=true&watch=true", 188 }, 189 }, 190 expectedList: &unstructured.UnstructuredList{ 191 Object: map[string]interface{}{ 192 "apiVersion": "", 193 "kind": "UnstructuredList", 194 "metadata": map[string]interface{}{ 195 "resourceVersion": "10", 196 }, 197 }, 198 Items: []unstructured.Unstructured{ 199 *getObject("gtest/vTest", "rTest", "item1"), 200 *getObject("gtest/vTest", "rTest", "item2"), 201 }, 202 }, 203 }, 204 { 205 name: "watch-list request for namespaced watch resource", 206 namespace: "nstest", 207 watchResponse: []watch.Event{ 208 {Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "item1")}, 209 {Type: watch.Bookmark, Object: func() runtime.Object { 210 obj := getObject("gtest/vTest", "rTest", "item2") 211 obj.SetResourceVersion("39") 212 obj.SetAnnotations(map[string]string{metav1.InitialEventsAnnotationKey: "true"}) 213 return obj 214 }()}, 215 }, 216 expectedRequestParams: []requestParam{ 217 { 218 Path: "/apis/gtest/vtest/namespaces/nstest/rtest", 219 Query: "allowWatchBookmarks=true&resourceVersionMatch=NotOlderThan&sendInitialEvents=true&watch=true", 220 }, 221 }, 222 expectedList: &unstructured.UnstructuredList{ 223 Object: map[string]interface{}{ 224 "apiVersion": "", 225 "kind": "UnstructuredList", 226 "metadata": map[string]interface{}{ 227 "resourceVersion": "39", 228 }, 229 }, 230 Items: []unstructured.Unstructured{ 231 *getObject("gtest/vTest", "rTest", "item1"), 232 }, 233 }, 234 }, 235 { 236 name: "watch-list request falls back to standard list on any error", 237 namespace: "nstest", 238 // watchList method in client-go expect only watch.Add and watch.Bookmark events 239 // receiving watch.Error will cause this method to report an error which will 240 // trigger the fallback logic 241 watchResponse: []watch.Event{ 242 {Type: watch.Error, Object: getObject("gtest/vTest", "rTest", "item1")}, 243 }, 244 listResponse: getListJSON("vTest", "UnstructuredList", 245 getJSON("gtest/vTest", "rTest", "item1"), 246 getJSON("gtest/vTest", "rTest", "item2")), 247 expectedRequestParams: []requestParam{ 248 // a watch-list request first 249 { 250 Path: "/apis/gtest/vtest/namespaces/nstest/rtest", 251 Query: "allowWatchBookmarks=true&resourceVersionMatch=NotOlderThan&sendInitialEvents=true&watch=true", 252 }, 253 // a standard list request second 254 { 255 Path: "/apis/gtest/vtest/namespaces/nstest/rtest", 256 }, 257 }, 258 expectedList: &unstructured.UnstructuredList{ 259 Object: map[string]interface{}{ 260 "apiVersion": "vTest", 261 "kind": "UnstructuredList", 262 }, 263 Items: []unstructured.Unstructured{ 264 *getObject("gtest/vTest", "rTest", "item1"), 265 *getObject("gtest/vTest", "rTest", "item2"), 266 }, 267 }, 268 }, 269 } 270 for _, scenario := range scenarios { 271 t.Run(scenario.name, func(t *testing.T) { 272 var actualRequestParams []requestParam 273 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"} 274 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 275 if r.Method != "GET" { 276 t.Errorf("unexpected HTTP method %s. expected GET", r.Method) 277 } 278 actualRequestParams = append(actualRequestParams, requestParam{ 279 Path: r.URL.Path, 280 Query: r.URL.RawQuery, 281 }) 282 283 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 284 // handle LIST response 285 if len(scenario.listResponse) > 0 { 286 if _, err := w.Write(scenario.listResponse); err != nil { 287 t.Fatal(err) 288 } 289 return 290 } 291 292 // handle WATCH response 293 enc := restclientwatch.NewEncoder(streaming.NewEncoder(w, unstructured.UnstructuredJSONScheme), unstructured.UnstructuredJSONScheme) 294 for _, e := range scenario.watchResponse { 295 if err := enc.Encode(&e); err != nil { 296 t.Fatal(err) 297 } 298 } 299 }) 300 if err != nil { 301 t.Fatalf("unexpected error when creating test client and server: %v", err) 302 } 303 defer srv.Close() 304 305 actualList, err := cl.Resource(resource).Namespace(scenario.namespace).List(context.TODO(), metav1.ListOptions{}) 306 if err != nil { 307 t.Fatalf("unexpected error: %v", err) 308 } 309 310 if !cmp.Equal(scenario.expectedRequestParams, actualRequestParams) { 311 t.Fatalf("unexpected request params: %v", cmp.Diff(scenario.expectedRequestParams, actualRequestParams)) 312 } 313 if !cmp.Equal(scenario.expectedList, actualList) { 314 t.Errorf("received expected list, diff: %s", cmp.Diff(scenario.expectedList, actualList)) 315 } 316 }) 317 } 318 } 319 320 func TestGet(t *testing.T) { 321 tcs := []struct { 322 resource string 323 subresource []string 324 namespace string 325 name string 326 path string 327 resp []byte 328 want *unstructured.Unstructured 329 }{ 330 { 331 resource: "rtest", 332 name: "normal_get", 333 path: "/apis/gtest/vtest/rtest/normal_get", 334 resp: getJSON("vTest", "rTest", "normal_get"), 335 want: getObject("vTest", "rTest", "normal_get"), 336 }, 337 { 338 resource: "rtest", 339 namespace: "nstest", 340 name: "namespaced_get", 341 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_get", 342 resp: getJSON("vTest", "rTest", "namespaced_get"), 343 want: getObject("vTest", "rTest", "namespaced_get"), 344 }, 345 { 346 resource: "rtest", 347 subresource: []string{"srtest"}, 348 name: "normal_subresource_get", 349 path: "/apis/gtest/vtest/rtest/normal_subresource_get/srtest", 350 resp: getJSON("vTest", "srTest", "normal_subresource_get"), 351 want: getObject("vTest", "srTest", "normal_subresource_get"), 352 }, 353 { 354 resource: "rtest", 355 subresource: []string{"srtest"}, 356 namespace: "nstest", 357 name: "namespaced_subresource_get", 358 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_get/srtest", 359 resp: getJSON("vTest", "srTest", "namespaced_subresource_get"), 360 want: getObject("vTest", "srTest", "namespaced_subresource_get"), 361 }, 362 } 363 for _, tc := range tcs { 364 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource} 365 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 366 if r.Method != "GET" { 367 t.Errorf("Get(%q) got HTTP method %s. wanted GET", tc.name, r.Method) 368 } 369 370 if r.URL.Path != tc.path { 371 t.Errorf("Get(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 372 } 373 374 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 375 w.Write(tc.resp) 376 }) 377 if err != nil { 378 t.Errorf("unexpected error when creating client: %v", err) 379 continue 380 } 381 defer srv.Close() 382 383 got, err := cl.Resource(resource).Namespace(tc.namespace).Get(context.TODO(), tc.name, metav1.GetOptions{}, tc.subresource...) 384 if err != nil { 385 t.Errorf("unexpected error when getting %q: %v", tc.name, err) 386 continue 387 } 388 389 if !reflect.DeepEqual(got, tc.want) { 390 t.Errorf("Get(%q) want: %v\ngot: %v", tc.name, tc.want, got) 391 } 392 } 393 } 394 395 func TestDelete(t *testing.T) { 396 background := metav1.DeletePropagationBackground 397 uid := types.UID("uid") 398 399 statusOK := &metav1.Status{ 400 TypeMeta: metav1.TypeMeta{Kind: "Status"}, 401 Status: metav1.StatusSuccess, 402 } 403 tcs := []struct { 404 subresource []string 405 namespace string 406 name string 407 path string 408 deleteOptions metav1.DeleteOptions 409 }{ 410 { 411 name: "normal_delete", 412 path: "/apis/gtest/vtest/rtest/normal_delete", 413 }, 414 { 415 namespace: "nstest", 416 name: "namespaced_delete", 417 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete", 418 }, 419 { 420 subresource: []string{"srtest"}, 421 name: "normal_delete", 422 path: "/apis/gtest/vtest/rtest/normal_delete/srtest", 423 }, 424 { 425 subresource: []string{"srtest"}, 426 namespace: "nstest", 427 name: "namespaced_delete", 428 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete/srtest", 429 }, 430 { 431 namespace: "nstest", 432 name: "namespaced_delete_with_options", 433 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete_with_options", 434 deleteOptions: metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &uid}, PropagationPolicy: &background}, 435 }, 436 } 437 for _, tc := range tcs { 438 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"} 439 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 440 if r.Method != "DELETE" { 441 t.Errorf("Delete(%q) got HTTP method %s. wanted DELETE", tc.name, r.Method) 442 } 443 444 if r.URL.Path != tc.path { 445 t.Errorf("Delete(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 446 } 447 448 content := r.Header.Get("Content-Type") 449 if content != runtime.ContentTypeJSON { 450 t.Errorf("Delete(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON) 451 } 452 453 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 454 unstructured.UnstructuredJSONScheme.Encode(statusOK, w) 455 }) 456 if err != nil { 457 t.Errorf("unexpected error when creating client: %v", err) 458 continue 459 } 460 defer srv.Close() 461 462 err = cl.Resource(resource).Namespace(tc.namespace).Delete(context.TODO(), tc.name, tc.deleteOptions, tc.subresource...) 463 if err != nil { 464 t.Errorf("unexpected error when deleting %q: %v", tc.name, err) 465 continue 466 } 467 } 468 } 469 470 func TestDeleteCollection(t *testing.T) { 471 statusOK := &metav1.Status{ 472 TypeMeta: metav1.TypeMeta{Kind: "Status"}, 473 Status: metav1.StatusSuccess, 474 } 475 tcs := []struct { 476 namespace string 477 name string 478 path string 479 }{ 480 { 481 name: "normal_delete_collection", 482 path: "/apis/gtest/vtest/rtest", 483 }, 484 { 485 namespace: "nstest", 486 name: "namespaced_delete_collection", 487 path: "/apis/gtest/vtest/namespaces/nstest/rtest", 488 }, 489 } 490 for _, tc := range tcs { 491 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"} 492 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 493 if r.Method != "DELETE" { 494 t.Errorf("DeleteCollection(%q) got HTTP method %s. wanted DELETE", tc.name, r.Method) 495 } 496 497 if r.URL.Path != tc.path { 498 t.Errorf("DeleteCollection(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 499 } 500 501 content := r.Header.Get("Content-Type") 502 if content != runtime.ContentTypeJSON { 503 t.Errorf("DeleteCollection(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON) 504 } 505 506 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 507 unstructured.UnstructuredJSONScheme.Encode(statusOK, w) 508 }) 509 if err != nil { 510 t.Errorf("unexpected error when creating client: %v", err) 511 continue 512 } 513 defer srv.Close() 514 515 err = cl.Resource(resource).Namespace(tc.namespace).DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{}) 516 if err != nil { 517 t.Errorf("unexpected error when deleting collection %q: %v", tc.name, err) 518 continue 519 } 520 } 521 } 522 523 func TestCreate(t *testing.T) { 524 tcs := []struct { 525 resource string 526 subresource []string 527 name string 528 namespace string 529 obj *unstructured.Unstructured 530 path string 531 }{ 532 { 533 resource: "rtest", 534 name: "normal_create", 535 path: "/apis/gtest/vtest/rtest", 536 obj: getObject("gtest/vTest", "rTest", "normal_create"), 537 }, 538 { 539 resource: "rtest", 540 name: "namespaced_create", 541 namespace: "nstest", 542 path: "/apis/gtest/vtest/namespaces/nstest/rtest", 543 obj: getObject("gtest/vTest", "rTest", "namespaced_create"), 544 }, 545 { 546 resource: "rtest", 547 subresource: []string{"srtest"}, 548 name: "normal_subresource_create", 549 path: "/apis/gtest/vtest/rtest/normal_subresource_create/srtest", 550 obj: getObject("vTest", "srTest", "normal_subresource_create"), 551 }, 552 { 553 resource: "rtest/", 554 subresource: []string{"srtest"}, 555 name: "namespaced_subresource_create", 556 namespace: "nstest", 557 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_create/srtest", 558 obj: getObject("vTest", "srTest", "namespaced_subresource_create"), 559 }, 560 } 561 for _, tc := range tcs { 562 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource} 563 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 564 if r.Method != "POST" { 565 t.Errorf("Create(%q) got HTTP method %s. wanted POST", tc.name, r.Method) 566 } 567 568 if r.URL.Path != tc.path { 569 t.Errorf("Create(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 570 } 571 572 content := r.Header.Get("Content-Type") 573 if content != runtime.ContentTypeJSON { 574 t.Errorf("Create(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON) 575 } 576 577 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 578 data, err := io.ReadAll(r.Body) 579 if err != nil { 580 t.Errorf("Create(%q) unexpected error reading body: %v", tc.name, err) 581 w.WriteHeader(http.StatusInternalServerError) 582 return 583 } 584 585 w.Write(data) 586 }) 587 if err != nil { 588 t.Errorf("unexpected error when creating client: %v", err) 589 continue 590 } 591 defer srv.Close() 592 593 got, err := cl.Resource(resource).Namespace(tc.namespace).Create(context.TODO(), tc.obj, metav1.CreateOptions{}, tc.subresource...) 594 if err != nil { 595 t.Errorf("unexpected error when creating %q: %v", tc.name, err) 596 continue 597 } 598 599 if !reflect.DeepEqual(got, tc.obj) { 600 t.Errorf("Create(%q) want: %v\ngot: %v", tc.name, tc.obj, got) 601 } 602 } 603 } 604 605 func TestUpdate(t *testing.T) { 606 tcs := []struct { 607 resource string 608 subresource []string 609 name string 610 namespace string 611 obj *unstructured.Unstructured 612 path string 613 }{ 614 { 615 resource: "rtest", 616 name: "normal_update", 617 path: "/apis/gtest/vtest/rtest/normal_update", 618 obj: getObject("gtest/vTest", "rTest", "normal_update"), 619 }, 620 { 621 resource: "rtest", 622 name: "namespaced_update", 623 namespace: "nstest", 624 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_update", 625 obj: getObject("gtest/vTest", "rTest", "namespaced_update"), 626 }, 627 { 628 resource: "rtest", 629 subresource: []string{"srtest"}, 630 name: "normal_subresource_update", 631 path: "/apis/gtest/vtest/rtest/normal_update/srtest", 632 obj: getObject("gtest/vTest", "srTest", "normal_update"), 633 }, 634 { 635 resource: "rtest", 636 subresource: []string{"srtest"}, 637 name: "namespaced_subresource_update", 638 namespace: "nstest", 639 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_update/srtest", 640 obj: getObject("gtest/vTest", "srTest", "namespaced_update"), 641 }, 642 } 643 for _, tc := range tcs { 644 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource} 645 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 646 if r.Method != "PUT" { 647 t.Errorf("Update(%q) got HTTP method %s. wanted PUT", tc.name, r.Method) 648 } 649 650 if r.URL.Path != tc.path { 651 t.Errorf("Update(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 652 } 653 654 content := r.Header.Get("Content-Type") 655 if content != runtime.ContentTypeJSON { 656 t.Errorf("Uppdate(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON) 657 } 658 659 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 660 data, err := io.ReadAll(r.Body) 661 if err != nil { 662 t.Errorf("Update(%q) unexpected error reading body: %v", tc.name, err) 663 w.WriteHeader(http.StatusInternalServerError) 664 return 665 } 666 667 w.Write(data) 668 }) 669 if err != nil { 670 t.Errorf("unexpected error when creating client: %v", err) 671 continue 672 } 673 defer srv.Close() 674 675 got, err := cl.Resource(resource).Namespace(tc.namespace).Update(context.TODO(), tc.obj, metav1.UpdateOptions{}, tc.subresource...) 676 if err != nil { 677 t.Errorf("unexpected error when updating %q: %v", tc.name, err) 678 continue 679 } 680 681 if !reflect.DeepEqual(got, tc.obj) { 682 t.Errorf("Update(%q) want: %v\ngot: %v", tc.name, tc.obj, got) 683 } 684 } 685 } 686 687 func TestWatch(t *testing.T) { 688 tcs := []struct { 689 name string 690 namespace string 691 events []watch.Event 692 path string 693 query string 694 }{ 695 { 696 name: "normal_watch", 697 path: "/apis/gtest/vtest/rtest", 698 query: "watch=true", 699 events: []watch.Event{ 700 {Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "normal_watch")}, 701 {Type: watch.Modified, Object: getObject("gtest/vTest", "rTest", "normal_watch")}, 702 {Type: watch.Deleted, Object: getObject("gtest/vTest", "rTest", "normal_watch")}, 703 }, 704 }, 705 { 706 name: "namespaced_watch", 707 namespace: "nstest", 708 path: "/apis/gtest/vtest/namespaces/nstest/rtest", 709 query: "watch=true", 710 events: []watch.Event{ 711 {Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")}, 712 {Type: watch.Modified, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")}, 713 {Type: watch.Deleted, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")}, 714 }, 715 }, 716 } 717 for _, tc := range tcs { 718 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"} 719 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 720 if r.Method != "GET" { 721 t.Errorf("Watch(%q) got HTTP method %s. wanted GET", tc.name, r.Method) 722 } 723 724 if r.URL.Path != tc.path { 725 t.Errorf("Watch(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 726 } 727 if r.URL.RawQuery != tc.query { 728 t.Errorf("Watch(%q) got query %s. wanted %s", tc.name, r.URL.RawQuery, tc.query) 729 } 730 731 w.Header().Set("Content-Type", "application/json") 732 733 enc := restclientwatch.NewEncoder(streaming.NewEncoder(w, unstructured.UnstructuredJSONScheme), unstructured.UnstructuredJSONScheme) 734 for _, e := range tc.events { 735 enc.Encode(&e) 736 } 737 }) 738 if err != nil { 739 t.Errorf("unexpected error when creating client: %v", err) 740 continue 741 } 742 defer srv.Close() 743 744 watcher, err := cl.Resource(resource).Namespace(tc.namespace).Watch(context.TODO(), metav1.ListOptions{}) 745 if err != nil { 746 t.Errorf("unexpected error when watching %q: %v", tc.name, err) 747 continue 748 } 749 750 for _, want := range tc.events { 751 got := <-watcher.ResultChan() 752 if !reflect.DeepEqual(got, want) { 753 t.Errorf("Watch(%q) want: %v\ngot: %v", tc.name, want, got) 754 } 755 } 756 } 757 } 758 759 func TestPatch(t *testing.T) { 760 tcs := []struct { 761 resource string 762 subresource []string 763 name string 764 namespace string 765 patch []byte 766 want *unstructured.Unstructured 767 path string 768 }{ 769 { 770 resource: "rtest", 771 name: "normal_patch", 772 path: "/apis/gtest/vtest/rtest/normal_patch", 773 patch: getJSON("gtest/vTest", "rTest", "normal_patch"), 774 want: getObject("gtest/vTest", "rTest", "normal_patch"), 775 }, 776 { 777 resource: "rtest", 778 name: "namespaced_patch", 779 namespace: "nstest", 780 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_patch", 781 patch: getJSON("gtest/vTest", "rTest", "namespaced_patch"), 782 want: getObject("gtest/vTest", "rTest", "namespaced_patch"), 783 }, 784 { 785 resource: "rtest", 786 subresource: []string{"srtest"}, 787 name: "normal_subresource_patch", 788 path: "/apis/gtest/vtest/rtest/normal_subresource_patch/srtest", 789 patch: getJSON("gtest/vTest", "srTest", "normal_subresource_patch"), 790 want: getObject("gtest/vTest", "srTest", "normal_subresource_patch"), 791 }, 792 { 793 resource: "rtest", 794 subresource: []string{"srtest"}, 795 name: "namespaced_subresource_patch", 796 namespace: "nstest", 797 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_patch/srtest", 798 patch: getJSON("gtest/vTest", "srTest", "namespaced_subresource_patch"), 799 want: getObject("gtest/vTest", "srTest", "namespaced_subresource_patch"), 800 }, 801 } 802 for _, tc := range tcs { 803 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource} 804 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 805 if r.Method != "PATCH" { 806 t.Errorf("Patch(%q) got HTTP method %s. wanted PATCH", tc.name, r.Method) 807 } 808 809 if r.URL.Path != tc.path { 810 t.Errorf("Patch(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 811 } 812 813 content := r.Header.Get("Content-Type") 814 if content != string(types.StrategicMergePatchType) { 815 t.Errorf("Patch(%q) got Content-Type %s. wanted %s", tc.name, content, types.StrategicMergePatchType) 816 } 817 818 data, err := io.ReadAll(r.Body) 819 if err != nil { 820 t.Errorf("Patch(%q) unexpected error reading body: %v", tc.name, err) 821 w.WriteHeader(http.StatusInternalServerError) 822 return 823 } 824 825 w.Header().Set("Content-Type", "application/json") 826 w.Write(data) 827 }) 828 if err != nil { 829 t.Errorf("unexpected error when creating client: %v", err) 830 continue 831 } 832 defer srv.Close() 833 834 got, err := cl.Resource(resource).Namespace(tc.namespace).Patch(context.TODO(), tc.name, types.StrategicMergePatchType, tc.patch, metav1.PatchOptions{}, tc.subresource...) 835 if err != nil { 836 t.Errorf("unexpected error when patching %q: %v", tc.name, err) 837 continue 838 } 839 840 if !reflect.DeepEqual(got, tc.want) { 841 t.Errorf("Patch(%q) want: %v\ngot: %v", tc.name, tc.want, got) 842 } 843 } 844 } 845 846 func TestInvalidSegments(t *testing.T) { 847 name := "bad/name" 848 namespace := "bad/namespace" 849 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"} 850 obj := &unstructured.Unstructured{ 851 Object: map[string]interface{}{ 852 "apiVersion": "vtest", 853 "kind": "vkind", 854 "metadata": map[string]interface{}{ 855 "name": name, 856 }, 857 }, 858 } 859 cl, err := NewForConfig(&restclient.Config{ 860 Host: "127.0.0.1", 861 }) 862 if err != nil { 863 t.Fatalf("Failed to create config: %v", err) 864 } 865 866 _, err = cl.Resource(resource).Namespace(namespace).Create(context.TODO(), obj, metav1.CreateOptions{}) 867 if err == nil || !strings.Contains(err.Error(), "invalid namespace") { 868 t.Fatalf("Expected `invalid namespace` error, got: %v", err) 869 } 870 871 _, err = cl.Resource(resource).Update(context.TODO(), obj, metav1.UpdateOptions{}) 872 if err == nil || !strings.Contains(err.Error(), "invalid resource name") { 873 t.Fatalf("Expected `invalid resource name` error, got: %v", err) 874 } 875 _, err = cl.Resource(resource).Namespace(namespace).Update(context.TODO(), obj, metav1.UpdateOptions{}) 876 if err == nil || !strings.Contains(err.Error(), "invalid namespace") { 877 t.Fatalf("Expected `invalid namespace` error, got: %v", err) 878 } 879 880 _, err = cl.Resource(resource).UpdateStatus(context.TODO(), obj, metav1.UpdateOptions{}) 881 if err == nil || !strings.Contains(err.Error(), "invalid resource name") { 882 t.Fatalf("Expected `invalid resource name` error, got: %v", err) 883 } 884 _, err = cl.Resource(resource).Namespace(namespace).UpdateStatus(context.TODO(), obj, metav1.UpdateOptions{}) 885 if err == nil || !strings.Contains(err.Error(), "invalid namespace") { 886 t.Fatalf("Expected `invalid namespace` error, got: %v", err) 887 } 888 889 err = cl.Resource(resource).Delete(context.TODO(), name, metav1.DeleteOptions{}) 890 if err == nil || !strings.Contains(err.Error(), "invalid resource name") { 891 t.Fatalf("Expected `invalid resource name` error, got: %v", err) 892 } 893 err = cl.Resource(resource).Namespace(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) 894 if err == nil || !strings.Contains(err.Error(), "invalid namespace") { 895 t.Fatalf("Expected `invalid namespace` error, got: %v", err) 896 } 897 898 err = cl.Resource(resource).Namespace(namespace).DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{}) 899 if err == nil || !strings.Contains(err.Error(), "invalid namespace") { 900 t.Fatalf("Expected `invalid namespace` error, got: %v", err) 901 } 902 903 _, err = cl.Resource(resource).Get(context.TODO(), name, metav1.GetOptions{}) 904 if err == nil || !strings.Contains(err.Error(), "invalid resource name") { 905 t.Fatalf("Expected `invalid resource name` error, got: %v", err) 906 } 907 _, err = cl.Resource(resource).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{}) 908 if err == nil || !strings.Contains(err.Error(), "invalid namespace") { 909 t.Fatalf("Expected `invalid namespace` error, got: %v", err) 910 } 911 912 _, err = cl.Resource(resource).Namespace(namespace).List(context.TODO(), metav1.ListOptions{}) 913 if err == nil || !strings.Contains(err.Error(), "invalid namespace") { 914 t.Fatalf("Expected `invalid namespace` error, got: %v", err) 915 } 916 917 _, err = cl.Resource(resource).Namespace(namespace).Watch(context.TODO(), metav1.ListOptions{}) 918 if err == nil || !strings.Contains(err.Error(), "invalid namespace") { 919 t.Fatalf("Expected `invalid namespace` error, got: %v", err) 920 } 921 922 _, err = cl.Resource(resource).Patch(context.TODO(), name, types.StrategicMergePatchType, []byte("{}"), metav1.PatchOptions{}) 923 if err == nil || !strings.Contains(err.Error(), "invalid resource name") { 924 t.Fatalf("Expected `invalid resource name` error, got: %v", err) 925 } 926 _, err = cl.Resource(resource).Namespace(namespace).Patch(context.TODO(), name, types.StrategicMergePatchType, []byte("{}"), metav1.PatchOptions{}) 927 if err == nil || !strings.Contains(err.Error(), "invalid namespace") { 928 t.Fatalf("Expected `invalid namespace` error, got: %v", err) 929 } 930 931 _, err = cl.Resource(resource).Apply(context.TODO(), name, obj, metav1.ApplyOptions{}) 932 if err == nil || !strings.Contains(err.Error(), "invalid resource name") { 933 t.Fatalf("Expected `invalid resource name` error, got: %v", err) 934 } 935 _, err = cl.Resource(resource).Namespace(namespace).Apply(context.TODO(), name, obj, metav1.ApplyOptions{}) 936 if err == nil || !strings.Contains(err.Error(), "invalid namespace") { 937 t.Fatalf("Expected `invalid namespace` error, got: %v", err) 938 } 939 940 _, err = cl.Resource(resource).ApplyStatus(context.TODO(), name, obj, metav1.ApplyOptions{}) 941 if err == nil || !strings.Contains(err.Error(), "invalid resource name") { 942 t.Fatalf("Expected `invalid resource name` error, got: %v", err) 943 } 944 _, err = cl.Resource(resource).Namespace(namespace).ApplyStatus(context.TODO(), name, obj, metav1.ApplyOptions{}) 945 if err == nil || !strings.Contains(err.Error(), "invalid namespace") { 946 t.Fatalf("Expected `invalid namespace` error, got: %v", err) 947 } 948 }